{"openapi":"3.0.3","info":{"title":"Floqer Public API","description":"Programmatic access to Floqer workflows and actions.\n\n## Recommended starting point for AI agents\n\nIf you're building workflows with this API, the easiest entry point is [`/docs/api/llms-full.txt`](/docs/api/llms-full.txt) — a single flattened text file that bundles Floqer's domain concepts (workflows, sheets, the action chain, reference tokens, connections), the full action catalog with configuration shapes and dynamic-options bindings, the API guide, and this OpenAPI spec.\n\n```\nGET /docs/api/llms-full.txt\n```\n\nLoading it once gives you the context you'd otherwise stitch together from many endpoints — how `{{input.X}}` and `{{<action_instance_id>.X}}` reference tokens flow through `inputs` and `outputs`, which fields on which actions are dynamic (and how to fetch their option lists), and the typical order operations happen in (Create Workflow → Add Inputs → Add Action → Get Options → Configure Action → Add Rows → Run). Use this OpenAPI for endpoint-level schema details once you have the bigger picture.\n\n---\n\n## Authentication\nAll endpoints require a Bearer token (API key starting with `floq_`). Pass it in the `Authorization` header:\n```\nAuthorization: Bearer floq_your_api_key\n```\n\n## Rate Limits\nDefault: **200 requests/minute**, **50,000 requests/day** per API key. Rate limit headers are included in every response:\n\n| Header | Description |\n|--------|-------------|\n| `X-RateLimit-Limit` | Max requests allowed in the current window |\n| `X-RateLimit-Remaining` | Requests remaining in the current window |\n| `X-RateLimit-Reset` | Unix timestamp when the window resets |\n| `Retry-After` | Seconds to wait (only on 429 responses) |\n\n## Response Envelope\nAll successful responses are wrapped in a standard envelope:\n```json\n{\n  \"status\": 200,\n  \"data\": { ... },\n  \"message\": \"Optional success message\"\n}\n```\n\n## Errors\nError responses use a consistent envelope:\n```json\n{\n  \"status\": 400,\n  \"error\": \"Bad Request\",\n  \"message\": \"Descriptive error message\"\n}\n```\n\nValidation errors (400) from source preview/create and other batched validators return every field failure at once:\n```json\n{\n  \"status\": 400,\n  \"error\": \"Validation Error\",\n  \"message\": \"2 validation errors\",\n  \"errors\": [{\"field\": \"filters.job_levels\", \"message\": \"unknown key for find_people\"}]\n}\n```\n\n| Status | Meaning |\n|--------|---------|\n| `400` | Bad Request — invalid parameters or missing required fields |\n| `401` | Unauthorized — missing or invalid API key |\n| `403` | Forbidden — API key lacks the required scope or access |\n| `404` | Not Found — resource does not exist |\n| `429` | Too Many Requests — rate limit exceeded |\n| `500` | Internal Server Error — unexpected failure |\n\n## Pagination\nList endpoints use cursor-based pagination with `pageSize` (default: 20) and `pageNo` (1-indexed). The response includes `totalCount`.\n\n## Scopes\nAPI keys are issued with permission scopes that control access:\n\n| Scope | Description |\n|-------|-------------|\n| `workflows:read` | List and view workflows |\n| `workflows:write` | Create, update, delete, and configure workflows |\n| `workflows:run` | Run and stop workflows |\n| `workflows:data:read` | Read workflow data rows and run history |\n| `workflows:data:write` | Add and delete workflow data rows |\n| `actions:read` | List available actions and view action configs |\n| `actions:write` | Add, save, and configure actions in workflows |\n| `sources:read` | Preview a data source, read imported source rows, and resolve dynamic field options (read-only — nothing is created) |\n| `sources:write` | Create sources, sync them to workflows, and pause or resume created sources |\n| `apps:read` | List apps, view an app's input/output schemas, and read runs and their results |\n| `apps:run` | Start app runs |\n| `campaigns:read` | List and read campaigns, their sequences, leads and metrics |\n| `campaigns:write` | Add leads, author sequence steps, publish, and pause or resume campaigns |\n| `ackdb` | Access to your org's ackDB API plus durable ackDB segment-to-workflow sync lifecycle operations |\n| `members:write` | Invite people to the organization and remove members. Carried only by an org admin's key |\n| `billing:read` | Read the credit balance and the usage breakdowns. Held by every role |\n\n---\n\n## Quick Start — Building a Workflow via API\n\n### 0. Bootstrap\n`GET /api/v1/user/` — confirm caller identity and org membership.\n\n### 1. Create a Workflow\n`POST /api/v1/workflows/` with `{\"name\": \"My Pipeline\"}` — returns `workflow_id`.\n\n### 2. Define Input Columns\n`POST /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/inputs` — add input fields (e.g. `linkedin_url`, `email`). For the main sheet, pass the workflow's own ID as `sheet_id`. Each input returns a `reference` token like `{{input.linkedin_url}}`.\n\n### 3. Add Actions\n`POST /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/add` — add actions by `action_id` (see `/docs/action-catalog.txt`). Returns `action_instance_id`, input fields to configure, and output `reference` tokens.\n\n### 4. Configure Action Inputs\n`PATCH /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/{action_instance_id}` — map variables using `{{input.<name>}}` or `{{<action_instance_id>.<name>}}` reference tokens.\n\n### 5. Add Data Rows\n`POST /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/rows` — insert rows using the **field names** from step 2. Returns `row_ids` for each inserted row.\n\n### 6. Run the Workflow\n`POST /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/run` — execute on specific rows:\n```json\n{\"row_ids\": [\"a1b2c3d4-...\", \"e5f6g7h8-...\"]}\n```\n\n### Variable Reference Cheat Sheet\n| Token | Example |\n|-------|--------|\n| Input reference | `{{input.linkedin_url}}` |\n| Action output reference | `{{scrape_person_linkedin_profile_1.full_name}}` |\n\n---\n\n## Working with Sheets\n\nSheets are child workflows linked to a parent. Each sheet has its own data, actions, and runs — manage it using any workflow endpoint with the sheet's own ID.\n\n### Create a Sheet\n`POST /api/v1/workflows/{workflow_id}/sheets` with `{\"name\": \"Sheet Name\"}` — returns `sheet_id`.\n\nUse the returned `sheet_id` (or the workflow's own ID for the main sheet) with sheet-scoped endpoints.\n\n### Get Workflow Overview\n`GET /api/v1/workflows/{workflow_id}` — returns workflow metadata, every sheet with settings, and webhook URLs for pushing rows. Use this as the bootstrap call after List Workflows.\n\n### Key Points\n- The main sheet's `sheet_id` equals the workflow's `workflow_id`.\n- Child sheets are created with `POST /workflows/{workflow_id}/sheets`.\n- Delete a child sheet with `DELETE /workflows/{workflow_id}/sheets/{sheet_id}`.\n- Deleting a sheet does not affect the parent or other sheets.\n\n---\n\n## Running Apps\n\nApps wrap a workflow behind a typed input/output schema, so you can run it with a single call — no workflow building required.\n\n### 1. Discover Apps\n`GET /api/v1/apps/` — list the apps you can run. Each app includes its `input_schema` and `output_schema`.\n\n### 2. Read the Input Schema\n`GET /api/v1/apps/{app_id}` — `input_schema` lists the fields to provide; use each field's `reference` (the snake_case field name, e.g. `linkedin_url`) as the key in `input_data`.\n\n### 3. Start a Run\n`POST /api/v1/apps/{app_id}/run` with `{\"input_data\": {\"<reference>\": \"<value>\"}}` — returns a run `id` immediately; the run executes asynchronously. To run many rows at once, `POST /api/v1/apps/{app_id}/run/batch` with `{\"rows\": [{\"<reference>\": \"<value>\"}, ...]}` (up to 1000 rows) — returns a `batch_id` grouping one run per row.\n\n### 4. Poll for Results\n`GET /api/v1/apps/{app_id}/runs/{run_id}` — poll until `status` is terminal (`completed`, `failed`, `error`, or `outOfCredits`); results are in `output_data`, keyed by each output field's `name` from the app's `output_schema`. `GET /api/v1/apps/{app_id}/runs` lists past runs — filter by `batch_id` to follow a batch.\n\n---\n\n## Running Campaigns\n\nA campaign runs leads through a sequence of email and task steps. Build the sequence, publish it, add leads, then read results.\n\n### 1. Pick a Campaign\n`GET /api/v1/campaigns` — campaigns you own or that are shared with you (`?scope=org` for the whole organization). `GET /api/v1/campaigns/{campaign_id}` returns `lead_schema` — the field names to use when adding leads.\n\n### 2. Build the Sequence\n`GET /api/v1/campaigns/sequence/catalog` — the step types you can add. Then `POST /api/v1/campaigns/{campaign_id}/sequence/nodes` with `{\"type\": \"send\", \"config\": {\"subject\": \"…\", \"body\": \"…\"}}` — appends a step, or pass `after` (a `node_id`) to insert. The delay between two steps is a `wait` step added for you; patch its `config` (`{delay, unit}`) with `PATCH .../sequence/nodes/{node_id}` to change a gap.\n\n### 3. Publish\n`POST /api/v1/campaigns/{campaign_id}/sequence/publish` — steps are written to a draft, and only the published sequence runs. Step ids are stable across publishes, so in-flight leads resume where they were.\n\n### 4. Add Leads\n`POST /api/v1/campaigns/{campaign_id}/leads` with `{\"leads\": [{\"email\": \"a@b.com\", \"company\": \"Acme\"}]}` (up to 1000 per call). `email` is the dedup key — re-adding an address updates it, so retries are safe.\n\n### 5. Send and Read Results\nA campaign sends only when it has a published sequence **and** `state` is `active` — `POST /api/v1/campaigns/{campaign_id}/resume` starts it (`/pause` holds it). Then `GET /api/v1/campaigns/{campaign_id}/metrics` for aggregate performance, `GET .../leads?status=replied` for a cohort, and `GET .../leads/{lead_id}` for one lead's step timeline.","version":"1.0.0","contact":{"name":"Floqer Support","url":"https://floqer.com"}},"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","description":"API key in the format: floq_..."}},"schemas":{"SuccessMessage":{"type":"object","description":"Simple success response with a confirmation message.","required":["message"],"properties":{"status":{"type":"integer","description":"HTTP status code","example":200},"message":{"type":"string","description":"Human-readable success message","example":"Workflow updated successfully"}}},"UserOrgMembers":{"type":"object","description":"Members of the caller's organization. Emails are Floqer usernames.","properties":{"count":{"type":"integer","description":"Number of members in the organization."},"emails":{"type":"array","items":{"type":"string","format":"email"},"description":"Email address of each org member (Floqer username), sorted alphabetically."}},"required":["count","emails"]},"User":{"type":"object","description":"Caller bootstrap payload. Identity and org membership are always present. Use List Workflows for accessible workflow metadata.","properties":{"email":{"type":"string","format":"email","description":"Caller's email address (Floqer username)."},"first_name":{"type":["null","string"],"description":"Caller's first name from their Floqer profile. `null` if unset."},"last_name":{"type":["null","string"],"description":"Caller's last name from their Floqer profile. `null` if unset."},"role":{"type":"string","enum":["admin","member","user"],"description":"Organization role: `admin` (org lead), `member`, or `user` (sources, shortcut runs, connections only). A label only; gate on `scopes`, which is authoritative (an unrecognised stored role reads as `member` with no scopes). Renamed from `operator` in STAP-3793."},"scopes":{"type":"array","items":{"type":"string"},"description":"Effective permissions for this session: the key's scopes narrowed to what the caller's role allows. `[\"*\"]` means everything. A call needing a scope not listed here is refused with `ROLE_FORBIDDEN`."},"org_id":{"type":"string","description":"UUID of the caller's organization."},"org_members":{"$ref":"#/components/schemas/UserOrgMembers"}},"required":["email","first_name","last_name","role","scopes","org_id","org_members"]},"UserKnowledge":{"type":"object","description":"The caller's organization knowledge file: a free-form text/markdown blob describing the org. `exists` is `false` and `content` is `null` when none has been written yet.","properties":{"content":{"type":["null","string"],"description":"The file's text content. `null` when no file has been written yet."},"exists":{"type":"boolean","description":"Whether a knowledge file currently exists for the org."},"updated_at":{"type":["null","string"],"format":"date-time","description":"ISO 8601 timestamp of the last write, or `null` when unset."}},"required":["content","exists","updated_at"]},"UserKnowledgeBody":{"type":"object","description":"Body for Set Knowledge. `content` replaces the entire knowledge file.","properties":{"content":{"type":"string","description":"The full text/markdown content of the knowledge file. Replaces any existing content. Send an empty string to clear it.","example":"# Floqer\n\nGTM automation platform. Our team runs enrichment + outreach workflows: enrich inbound signups, find work emails via waterfall, and sync qualified leads to HubSpot. Goal: cut manual research time and keep the CRM enriched."}},"required":["content"]},"OrgMemberInviteBody":{"type":"object","description":"Body for Add Member. The person is invited by email and joins when they accept.","properties":{"email":{"type":"string","minLength":1,"description":"Email address to invite. Matched case-insensitively against the existing roster.","example":"sam@acme.com"},"role":{"type":"string","enum":["admin","member"],"description":"Role the invitation carries. `admin` can manage members and billing, `member` can build and run workflows. The narrow third role (`user`) is not invitable over the API yet."}},"required":["email","role"],"additionalProperties":false},"OrgMemberInvite":{"type":"object","description":"A pending invitation. The invitee holds no access until they accept.","properties":{"email":{"type":"string","description":"The invited email address, lowercased."},"role":{"type":"string","enum":["admin","member","user"],"description":"Role the invitee receives on acceptance."},"status":{"type":"string","enum":["pending"],"description":"Always `pending` — acceptance happens in the Floqer app or at signup."},"email_sent":{"type":"boolean","description":"Whether the invitation email was accepted for delivery. `false` means the invitation is recorded and holds a seat but nobody was told — re-inviting returns `ALREADY_INVITED`, so revoke it with Remove Member and invite again."}},"required":["email","role","status","email_sent"]},"OrgMemberRemoval":{"type":"object","description":"What the call removed.","properties":{"email":{"type":"string","description":"The removed email address, lowercased."},"removed":{"type":"string","enum":["member","invitation"],"description":"`member` when an active member was removed, `invitation` when a pending invitation was revoked."},"warnings":{"type":"array","description":"What the removal broke or left behind. The removal still succeeded — these are consequences to act on, not failures. An empty array means none of the checks below found anything, not that the member was inconsequential.","items":{"type":"object","properties":{"code":{"type":"string","enum":["WORKFLOWS_LOST_CONNECTION","WORKFLOWS_LOST_OWNER","SOURCES_LOST_OWNER","IMPACT_UNKNOWN"],"description":"`WORKFLOWS_LOST_CONNECTION` — workflows used a connection they owned and now fail. `WORKFLOWS_LOST_OWNER` — workflows they own keep running; scheduled runs still bill the org. `SOURCES_LOST_OWNER` — active sources they owned stop importing. `IMPACT_UNKNOWN` — a lookup failed or was capped, so check their integrations by hand."},"message":{"type":"string","description":"Human-readable summary."},"resources":{"type":"array","description":"What is affected. Absent on `IMPACT_UNKNOWN`.","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string","enum":["workflow","draft","subfloq","source"]}},"required":["id","name","type"]}}},"required":["code","message"]}}},"required":["email","removed","warnings"]},"CreateWorkflowBody":{"type":"object","description":"Body for Create Workflow. Only `name` is required.","properties":{"name":{"type":"string","description":"Workflow name. Not required to be unique in the organization.","example":"Lead Enrichment Pipeline"},"folder_id":{"type":["string","null"],"minLength":1,"description":"Folder to place the workflow in, from List Workflow Folders. Omit it or pass `null` to place it at the top level."}},"required":["name"]},"Workflow":{"type":"object","description":"A workflow summary. Timestamps are ISO 8601 UTC; `last_run_at` is `null` until the workflow is first run.","properties":{"workflow_id":{"type":"string","format":"uuid","description":"UUID of the workflow. Use this with every endpoint that takes `{workflow_id}`."},"name":{"type":"string","description":"Workflow name as set at creation."},"created_at":{"type":"string","format":"date-time","description":"UTC timestamp (ISO 8601) when the workflow was created."},"updated_at":{"type":"string","format":"date-time","description":"UTC timestamp (ISO 8601) of the most recent configuration change (name, inputs, actions, sheet settings)."},"last_run_at":{"type":["null","string"],"format":"date-time","description":"UTC timestamp (ISO 8601) of the most recent run on any sheet in this workflow. `null` if it has never been run. Runs started outside this API — on a schedule, or by a webhook trigger — do not update it."},"folder_id":{"type":["null","string"],"description":"ID of the folder holding the workflow in the API key holder's workflow list. `null` when it sits at the top level."},"folder_path":{"type":["null","array"],"items":{"type":"string"},"description":"Folder names from the top-level folder down to the one holding the workflow, e.g. `[\"Reporting\", \"Weekly\"]`. `null` when `folder_id` is `null`."}},"required":["workflow_id","name","created_at","updated_at","last_run_at","folder_id","folder_path"]},"WorkflowDeleted":{"type":"object","description":"Confirmation envelope for a deleted workflow.","properties":{"workflow_id":{"type":"string","format":"uuid","description":"UUID of the workflow that was deleted."},"deleted":{"type":"boolean","description":"Always `true` on a 200 response. Included so agents can branch on a single field."}},"required":["workflow_id","deleted"]},"RenameWorkflowBody":{"type":"object","description":"Body for Rename Workflow. `name` is the only field.","properties":{"name":{"type":"string","minLength":1,"description":"New workflow name. Trimmed before saving; whitespace-only values are rejected with 400. Not required to be unique.","example":"Inbound lead enrichment"}},"required":["name"]},"WorkflowRenamed":{"type":"object","description":"Confirmation envelope for a renamed workflow. This is the workflow's own name; the main sheet's tab label is stored separately and is set via Rename Sheet.","properties":{"workflow_id":{"type":"string","format":"uuid","description":"UUID of the workflow that was renamed."},"name":{"type":"string","description":"The stored workflow name, after trimming.","example":"Inbound lead enrichment"}},"required":["workflow_id","name"]},"DuplicateWorkflowBody":{"type":"object","description":"Body for Duplicate Workflow. `name` becomes the copied workflow's name.","properties":{"name":{"type":"string","minLength":1,"description":"Name for the copy. Trimmed before saving and not required to be unique.","example":"Lead Enrichment Pipeline Copy"},"folder_id":{"type":["string","null"],"minLength":1,"description":"Folder to place the copy in, from List Workflow Folders. Omit it or pass `null` to place it at the top level."}},"required":["name"]},"WorkflowDuplicated":{"type":"object","description":"Identity of the newly-created workflow copy.","properties":{"workflow_id":{"type":"string","format":"uuid","description":"UUID of the new workflow copy."},"name":{"type":"string","description":"The copied workflow's stored name, after trimming.","example":"Lead Enrichment Pipeline Copy"}},"required":["workflow_id","name"]},"WorkflowOverviewSheet":{"type":"object","description":"A sheet's operational settings within the parent workflow returned by Get Workflow Overview. `sheet_id` is the UUID to pass on sheet-scoped endpoints.","properties":{"sheet_id":{"type":"string","format":"uuid","description":"UUID of the sheet. Pass as `{sheet_id}` on every sheet-scoped endpoint, and as `target_sheet_id` when another sheet routes data here via `send_to_sheet`."},"name":{"type":"string","description":"Sheet name as set at creation. Not required to be unique within a workflow."},"is_main_sheet":{"type":"boolean","description":"`true` for the workflow's main sheet — auto-created with the workflow, always present, and has `sheet_id === data.workflow_id`."},"auto_run":{"type":"boolean","description":"When `true` (default), the sheet's action chain runs automatically whenever new data rows arrive."},"cache_enabled":{"type":"boolean","description":"When `true` (default), a row with input column values identical to a previously-run row reuses the previous row's outputs instead of re-running the action chain."},"cache_since":{"type":["null","string"],"format":"date","description":"Earliest date (`YYYY-MM-DD`, UTC) from which cached runs are considered fresh. `null` means all cached runs are considered fresh regardless of age."},"continue_on_fail":{"type":"boolean","description":"When `true`, a row keeps moving to the next action after an action fails on it. When `false` (default), the row stops at the failing action. Every action follows this setting unless it has its own `continue_workflow_if_action_fails`."},"webhook_url":{"type":["null","string"],"format":"uri","description":"POST URL for pushing rows into this sheet via webhook. Send a JSON body whose keys match the sheet's input columns. `null` when the sheet has no webhook input configured."}},"required":["sheet_id","name","is_main_sheet","auto_run","cache_enabled","cache_since","continue_on_fail","webhook_url"]},"WorkflowOverview":{"type":"object","description":"Operational info for a workflow: metadata (same fields as List Workflows), sheet roster with settings and webhook URLs, and shared users.","properties":{"workflow_id":{"type":"string","format":"uuid","description":"UUID of the parent workflow."},"name":{"type":"string","description":"Workflow name as set at creation."},"created_at":{"type":"string","format":"date-time","description":"UTC timestamp (ISO 8601) when the workflow was created."},"updated_at":{"type":"string","format":"date-time","description":"UTC timestamp (ISO 8601) of the most recent configuration change (name, inputs, actions, sheet settings)."},"last_run_at":{"type":["null","string"],"format":"date-time","description":"UTC timestamp (ISO 8601) of the most recent run across every sheet in this workflow. `null` if no sheet has ever been run."},"folder_id":{"type":["null","string"],"description":"ID of the folder holding the workflow in the API key holder's workflow list. `null` when it sits at the top level."},"folder_path":{"type":["null","array"],"items":{"type":"string"},"description":"Folder names from the top-level folder down to the one holding the workflow, e.g. `[\"Reporting\", \"Weekly\"]`. `null` when `folder_id` is `null`."},"is_owner":{"type":"boolean","description":"`true` when the caller is the workflow owner."},"shared_users":{"type":"array","items":{"type":"string","format":"email"},"description":"Emails the workflow is shared with (edit access). Populated for the owner and org admins; otherwise `[]`."},"sheets":{"type":"array","items":{"$ref":"#/components/schemas/WorkflowOverviewSheet"},"description":"Every sheet in the workflow with its settings and webhook URL. `sheets[0]` is the main sheet."}},"required":["workflow_id","name","created_at","updated_at","last_run_at","folder_id","folder_path","is_owner","shared_users","sheets"]},"WorkflowFolder":{"type":"object","description":"A folder in the API key holder's workflow list.","properties":{"folder_id":{"type":"string","description":"Folder ID. Matches `folder_id` on the workflows inside it."},"name":{"type":"string","description":"Folder name."},"parent_folder_id":{"type":["null","string"],"description":"ID of the folder this one sits in. `null` for a top-level folder."},"path":{"type":"array","items":{"type":"string"},"description":"Folder names from the top-level folder down to and including this one."}},"required":["folder_id","name","parent_folder_id","path"]},"CreateWorkflowFolderBody":{"type":"object","description":"Body for Create Workflow Folder. Only `name` is required.","properties":{"name":{"type":"string","minLength":1,"description":"Folder name. Trimmed before saving and not required to be unique.","example":"Reporting"},"parent_folder_id":{"type":["string","null"],"minLength":1,"description":"Folder to create this one inside, from List Workflow Folders. Omit it or pass `null` for a top-level folder."}},"required":["name"]},"Sheet":{"type":"object","description":"A sheet in a workflow. Pass `sheet_id` as the `{sheet_id}` path parameter on every sheet-scoped endpoint.","properties":{"sheet_id":{"type":"string","format":"uuid","description":"UUID of the sheet. Pass as `{sheet_id}` on every sheet-scoped endpoint, and as `target_sheet_id` when another sheet routes data here via `send_to_sheet`.","example":"e5f6a7b8-c9d0-1e2f-3a4b-5c6d7e8f9a0b"},"workflow_id":{"type":"string","format":"uuid","description":"UUID of the parent workflow this sheet belongs to.","example":"531952c2-3d07-4be2-8c4b-733acba3187b"},"name":{"type":"string","description":"Sheet name as set at creation. Not required to be unique within a workflow.","example":"Employees"},"is_main_sheet":{"type":"boolean","description":"`true` for the workflow's main sheet — auto-created with the workflow, always present, cannot be deleted separately, and has `sheet_id === workflow_id`. `false` for additional sheets added via Create Sheet."},"auto_run":{"type":"boolean","description":"When `true` (default), the sheet's action chain runs automatically whenever new data rows arrive (via Add Data Rows or via a `send_to_sheet` action on another sheet). When `false`, runs must be triggered explicitly with Run Rows."},"cache_enabled":{"type":"boolean","description":"When `true` (default), a row with input column values identical to a previously-run row reuses the previous row's outputs instead of re-running the action chain. Matching compares every input column value on the row as a string, after variable references are resolved."},"cache_since":{"type":["null","string"],"format":"date","description":"Earliest date (`YYYY-MM-DD`, UTC) from which cached runs are considered fresh. Runs performed on or after this date are reusable; runs performed before are ignored even when inputs match. Ignored when `cache_enabled` is `false`. `null` means all cached runs are considered fresh regardless of age.","example":"2026-03-17"},"continue_on_fail":{"type":"boolean","description":"When `true`, a row keeps moving to the next action after an action fails on it. When `false` (default), the row stops at the failing action. Every action follows this setting unless it has its own `continue_workflow_if_action_fails`."}},"required":["sheet_id","workflow_id","name","is_main_sheet","auto_run","cache_enabled","cache_since","continue_on_fail"]},"CreateSheetBody":{"type":"object","description":"Body for Create Sheet. Only `name` is required.","properties":{"name":{"type":"string","description":"Sheet name. Not required to be unique within a workflow.","example":"Employees"},"auto_run":{"type":"boolean","description":"When `true` (default), the sheet's action chain runs automatically whenever new data rows arrive — via Add Data Rows or via a `send_to_sheet` action on another sheet. When `false`, runs must be triggered explicitly with Run Rows."},"cache_enabled":{"type":"boolean","description":"When `true` (default), a row with input column values identical to a previously-run row reuses the previous row's outputs instead of re-running the action chain. Set to `false` to always re-run."},"cache_since":{"type":["string","null"],"format":"date","description":"Earliest date (`YYYY-MM-DD`, UTC) from which cached runs are considered fresh. Ignored when `cache_enabled` is `false`. Default: `null` — all cached runs are considered fresh regardless of age.","example":"2026-03-17"},"continue_on_fail":{"type":"boolean","description":"When `true`, a row keeps moving to the next action after an action fails on it. When `false` (default), the row stops at the failing action. Every action follows this setting unless it has its own `continue_workflow_if_action_fails`."}},"required":["name"]},"SheetDeleted":{"type":"object","description":"Confirmation envelope for a deleted child sheet.","properties":{"workflow_id":{"type":"string","format":"uuid","description":"UUID of the parent workflow the sheet belonged to."},"sheet_id":{"type":"string","format":"uuid","description":"UUID of the child sheet that was deleted."},"deleted":{"type":"boolean","description":"Always `true` on a 200 response. Included so agents can branch on a single field."}},"required":["workflow_id","sheet_id","deleted"]},"RenameSheetBody":{"type":"object","description":"Body for Rename Sheet. `name` is the only field.","properties":{"name":{"type":"string","minLength":1,"description":"New sheet name. Trimmed before saving; whitespace-only values are rejected with 400. Not required to be unique within a workflow.","example":"Qualified employees"}},"required":["name"]},"SheetRenamed":{"type":"object","description":"Confirmation envelope for a renamed child sheet.","properties":{"workflow_id":{"type":"string","format":"uuid","description":"UUID of the parent workflow the sheet belongs to."},"sheet_id":{"type":"string","format":"uuid","description":"UUID of the child sheet that was renamed."},"name":{"type":"string","description":"The stored sheet name, after trimming.","example":"Qualified employees"}},"required":["workflow_id","sheet_id","name"]},"UpdateSheetBody":{"type":"object","description":"Body for Update Sheet. Send only the settings to change; at least one is required.","properties":{"auto_run":{"type":"boolean","description":"Run-on-receive toggle. `true` runs the action chain on every row that lands on the sheet (Add Data Rows, source syncs, `send_to_sheet`). `false` loads rows without running them; run them with Run Rows."},"cache_enabled":{"type":"boolean","description":"When `true`, a row with input column values identical to a previously-run row reuses the previous row's outputs instead of re-running the action chain. When `false`, every row triggers a fresh run."},"cache_since":{"type":["string","null"],"format":"date","description":"Earliest date (`YYYY-MM-DD`, UTC) from which cached runs are considered fresh. Runs before this date are ignored even when inputs match. `null` clears the cutoff — all cached runs are considered fresh regardless of age.","example":"2026-03-17"}},"minProperties":1,"additionalProperties":false},"Input":{"type":"object","description":"A single configured input field on a workflow sheet. Each input becomes a column in the sheet's data table and gets a `reference` token that downstream actions can use to pull the value.","properties":{"name":{"type":"string","description":"Input field name. Unique within the sheet. Used to build the `reference` token.","example":"linkedin_url"},"type":{"type":"string","enum":["string","url","email","number"],"description":"Input data type. One of: `string`, `url`, `email`, `number`.","example":"url"},"description":{"type":"string","description":"Human-readable description of the input. Empty string when not set.","example":"LinkedIn company profile URL"},"reference":{"type":"string","description":"Variable reference token. Use this value in downstream action `inputString`s to pull the input's value at run time. Always of the form `{{input.<name>}}`.","example":"{{input.linkedin_url}}"}},"required":["name","type","description","reference"]},"InputCreate":{"type":"object","description":"An input field to add to a workflow sheet. `reference` is server-computed from `name` and is never accepted on the request.","properties":{"name":{"type":"string","description":"Field name — used as the column header and as the key when adding data rows. Must be unique within the sheet.","example":"linkedin_url"},"type":{"type":"string","enum":["string","url","email","number"],"description":"Data type. Helps agents match fields to action inputs (e.g. an action that needs a URL can search for `type: url`). One of: `string`, `url`, `email`, `number`.","example":"url"},"description":{"type":"string","description":"Human-readable description of what this field contains.","example":"LinkedIn company profile URL"}},"required":["name","type"]},"ActionInput":{"type":"object","description":"A single input field on an action instance. The caller wires this field up via the Configure Action endpoint.","properties":{"name":{"type":"string","description":"Input field identifier (the template's `payloadStructureId`, lowercased).","example":"linkedin_url"},"type":{"type":"string","description":"Field type (e.g. `string`, `url`, `email`, `number`, `boolean`, `promptField`, `stepDownSearch`).","example":"url"},"required":{"type":"boolean","description":"Whether this field must be configured before the action can run."},"description":{"type":"string","description":"Human-readable description of what this field accepts.","example":"LinkedIn profile URL of the person"}},"required":["name","type","required"]},"ActionOutput":{"type":"object","description":"A single output field produced by an action instance when it runs. `reference` is the variable token downstream actions use to pull this value. Outputs of type `structured_array` (a list of structured rows) additionally surface their per-column schema under `columns`, each with its own reference token for addressing individual columns. New `http_api_call` actions expose exactly two object outputs: `response` (the complete target body) and `metadata` ({status: number, isJson: boolean, durationMs: number, sizeBytes: number, attempts: number}). A target 4xx/5xx remains a completed action result: its body is saved in `response` and its status in `metadata`; only transport, timeout, the 25 MB response-size limit, or Floqer processing failures fail the cell. Metadata is separate from the body; durationMs measures request/body-read time in milliseconds and sizeBytes is the UTF-8 body length. Reference nested body values with a path such as `{{http_api_call_1.response.data.email}}`; materializing one as a column is a separate downstream `formula` action. Rootless legacy HTTP actions may retain configured projection outputs and are not automatically migrated.","properties":{"name":{"type":"string","description":"Output field identifier (snake_case).","example":"full_name"},"type":{"type":"string","description":"Output data type (e.g. `string`, `url`, `number`, `raw_array`, `json`, `structured_array`). `structured_array` is a list of structured rows; the per-row schema is exposed via `columns`.","example":"string"},"reference":{"type":"string","description":"Variable reference token. Drop this verbatim into any downstream action's `inputString` (via Configure Action) to pull this output's value at run time. Shape: `{{<action_instance_id>.<name>}}`. For an individual column of a `structured_array`, use the 3-segment form `{{<action_instance_id>.<list>.<column>}}` (also exposed on each entry of `columns[].structured_array_reference`). For an `object` output (e.g. `http_api_call`'s `response`) append a JSON path to pull one value: `{{<action_instance_id>.<name>.<path>}}`, path = dot-delimited keys with `[i]` array indexes (`data.items[0].email`); a missing path resolves to blank.","example":"{{enrich_person_linkedin_profile_1.full_name}}"},"sample":{"description":"Optional captured sample for a user-defined output. HTTP row runs store their results in row outputs; use Get Row or List Rows to inspect the result after Save and run first cell. A stored ping may also supply this sample, but a separate Test Request or Preview is never required. Any JSON type."},"description":{"type":"string","description":"Human-readable description of this output.","example":"Work history as JSON. Each item: title, company, start_date, end_date, summary, url, company_domain, company_identifier"},"columns":{"type":"array","description":"Present only on `structured_array` outputs. One entry per column inside the list, with its own `structured_array_reference` token shaped `{{<action_instance_id>.<list>.<column>}}`. Empty / absent for outputs whose columns are user-defined and not yet configured (e.g. a freshly added `raw_to_structured_array` action).","items":{"type":"object","properties":{"name":{"type":"string","description":"Column identifier (snake_case)."},"type":{"type":"string","description":"Column data type."},"structured_array_reference":{"type":"string","description":"3-segment reference token for this column inside the parent `structured_array`. Shape: `{{<action_instance_id>.<list>.<column>}}`.","example":"{{raw_to_structured_array_1.list.first_name}}"}},"required":["name","type","structured_array_reference"]}}},"required":["name","type","reference"]},"ActionInstance":{"type":"object","description":"An action instance in a workflow sheet's chain. Returned from Add Action and Get Action; contains enough information to subsequently call Configure Action (to wire inputs) or to wire downstream actions to this one (via `outputs[].reference`).","properties":{"action_instance_id":{"type":"string","description":"Unique ID of this action instance within the sheet. Use this value as the `after` param when inserting further actions, and as the path param on Configure / Delete endpoints.","example":"scrape_person_linkedin_profile_1"},"action_id":{"type":"string","description":"Action template identifier this instance was created from.","example":"scrape_person_linkedin_profile"},"display_name":{"type":"string","description":"User-facing name. Defaults to the action template's name; can be overridden per instance in the future.","example":"Scrape Person LinkedIn Profile"},"inputs":{"type":"array","items":{"$ref":"#/components/schemas/ActionInput"},"description":"Input fields the caller must configure via Configure Action."},"outputs":{"type":"array","items":{"$ref":"#/components/schemas/ActionOutput"},"description":"Output fields this action produces when it runs. Each has a `reference` token for downstream wiring."}},"required":["action_instance_id","action_id","display_name","inputs","outputs"]},"AddActionBody":{"type":"object","description":"Body for Add Action. `action_id` is the template; `after` (optional) is an `action_instance_id`; `name` (optional) overrides the template's default display name.\n\nEverything Configure Action accepts — `inputs`, `run_if`, `continue_workflow_if_action_fails`, `note`, `output_types` — can be sent here too, so an action can be created and fully configured in one request instead of two. Same shapes, same semantics, same `warnings[]`.","properties":{"action_id":{"type":"string","description":"Action template identifier from the action catalog (e.g. `enrich_company_linkedin_profile`).","example":"enrich_company_linkedin_profile"},"after":{"type":"string","description":"Insert after this `action_instance_id`. Omit to append to the end of the chain. NOTE: this is an instance ID, not an `action_id`.","example":"enrich_company_linkedin_profile_1"},"name":{"type":"string","description":"Optional display name for the new action instance. Whitespace-only values are ignored.","example":"Enrich primary contact"},"inputs":{"type":"object","additionalProperties":true,"description":"Optional. Configure the action in the same request instead of following up with Configure Action — one call per action instead of two. Identical semantics to Configure Action's `inputs` (same accepted value shapes, same reference translation, same `warnings[]` codes), because it runs the same engine. Any warnings come back on the 201.\n\nReferences may point at actions upstream of this one; a reference to an action that runs later is reported as a `downstream_reference` warning. On a hard validation failure nothing is created — you do not get a half-configured action to clean up.","example":{"linkedin_url":"{{input.linkedin_url}}"}},"run_if":{"type":["object","null"],"description":"Optional gate deciding whether this action runs for a given row. Pass `null` to clear an existing condition. Omit to leave the existing condition unchanged.\n\n`conditions` is an array of AND/OR condition GROUPS — the same shape as the filter action's `path_conditions`. The outer array is groups, joined by each group's `combinator` (AND/OR); each group's `conditions` are leaf conditions, joined by each leaf's `combinator`. The first group's and first leaf's `combinator` is ignored. A single condition is just one group with one leaf.\n\n**Supported `operator` values depend on the variable's stored type** — the type is derived from the sheet's chain, so you don't pass it.\n\n- **Universal** (any type): `is`, `is not`, `is empty`, `is not empty`. `is empty` / `is not empty` ignore `values`.\n- **String / email / url / imageUrl**: `contains`, `does not contain`, `starts with`, `does not start with`, `ends with`, `does not end with`. Matching is case-insensitive.\n- **Number / currency**: `greater than`, `less than`, `greater than or equal to`, `less than or equal to`, `is between` (pass `[min, max]`).\n- **Date**: `is after`, `is before`, `is between` (pass `[start, end]`). Values are parsed by `moment(...)`.\n- **Array / structured_array / json**: `contains`, `does not contain`.\n\nPass `values` as an array of strings — number / date matchers coerce as needed.","properties":{"conditions":{"type":"array","description":"AND/OR condition groups. Outer array = groups (joined by each group's `combinator`); each group's `conditions` = leaf conditions (joined by each leaf's `combinator`). A single condition is one group with one leaf.","items":{"type":"object","properties":{"conditions":{"type":"array","items":{"type":"object","properties":{"variable":{"type":"string","description":"Reference token whose resolved value gets compared. Use `{{input.<field_name>}}` for a sheet input, or `{{<action_instance_id>.<field_name>}}` for an upstream action's output.","examples":["{{input.country}}","{{enrich_company_linkedin_profile_1.employee_count}}"]},"operator":{"type":"string","description":"Comparison operator. See the per-type list above.","examples":["is","greater than","is not empty","contains"]},"values":{"type":"array","description":"Values to compare against. Required for most operators; ignored by `is empty` / `is not empty`. For `is between`, pass `[low, high]`.","items":{},"examples":[["US","CA"],["100"]]},"combinator":{"type":"string","enum":["AND","OR"],"description":"Joins this leaf with the previous one in the group. Ignored on the first leaf."}},"required":["variable","operator"]}},"combinator":{"type":"string","enum":["AND","OR"],"description":"Joins this group with the previous one. Ignored on the first group."}},"required":["conditions"]}},"continue_workflow_if_run_condition_not_met":{"type":"boolean","description":"When `true`, downstream actions still run for a row even when this action's condition isn't met (the action itself is skipped). When `false` (default), the row stops at this action when the condition isn't met."}}},"continue_workflow_if_action_fails":{"type":["boolean","null"],"description":"When `true`, downstream actions still run for a row even if this action fails. When `false`, the row stops at this action on failure and downstream cells stay queued. Send `null` to clear the action's own setting so it follows the sheet's `continue_on_fail` (default off). Omit to leave it unchanged. Absent in responses means the action follows the sheet."},"note":{"type":"string","description":"Optional free-text note to save on the action. Same upsert behaviour as Save Action Note — overwrites any existing note. Whitespace-only values are ignored.","example":"Skip until Phase 2 — vendor still negotiating contract terms."},"output_types":{"type":"object","description":"Optional. Sets the declared type of outputs whose type is a per-action choice — today Formula's `result`. Map of output name (as Get Outputs returns it) → type: `string` (default), `number`, `boolean`, `date`, `currency`, `email`, `url`, `imageUrl` or `json`. The stored value is unchanged; the type decides which `run_if` / `filter` operators apply, and Get Outputs reports it. A condition takes its type when the condition is saved, so set the type before adding conditions on this output. Any other output name returns 400.","additionalProperties":{"type":"string","enum":["string","number","boolean","date","currency","email","url","imageUrl","json"]},"example":{"result":"number"}}},"required":["action_id"]},"ConfigureActionBody":{"type":"object","description":"Body for Configure Action. Send only the fields you want to set — unset fields keep their current value.","properties":{"inputs":{"type":"object","description":"Map of field name → value. Each entry's value is one of:\n- **string**: a literal, a `{{public.ref}}`, or a mix of both.\n- **boolean**: for radio / toggle fields.\n- **number**: for numeric fields such as `http_api_call.timeout_seconds`.\n- **string[]**: used by two field types: waterfall providers (an ordered list of provider **display names**, when the template field's `type === \"stepDownSearch\"`) and multi-dropdown / multi-select fields. The handler picks the right storage shape based on the action template's field `type`.\n- **Array of provider objects**: the waterfall form that carries bring-your-own-key state: `{ name, use_own_key? }`. `use_own_key: true` runs that provider on your organization's own credential for the vendor (connect it in the Floqer UI first) instead of Floqer's key, so the call bills your account rather than Floqer credits. When your org holds several credentials for a vendor, the active one is used; pick a specific one in the Floqer UI. Providers you list as plain strings keep whatever key state they already had, so sending the array to reorder never silently clears it. Get Action returns the field in the same mixed shape: providers on your own key come back as `{ name, use_own_key: true }`, the rest as plain names.\n- **Array of objects**: for `jsonArray` fields (e.g. Salesforce field/value pairs, Google Sheets column mappings). Each item is typically `{ name, value }`, where `value` can carry `{{public.ref}}` tokens. Extra keys per item are preserved verbatim. Reference translation runs on every string property. If you nest `{{public.refs}}` inside `value`, `prompt`, or any other string field on the item, they get translated independently.\n- **Classification questions**: `ai_classify.questions` accepts an array (or JSON string) of `{id, name, type, question, options?, levels?, yes?, no?}`. `id` is a stable UUID; `type` is `yes_no`, `pick_one` or `score`. Questions are literal configuration; put row references in `ai_classify.input`. Invalid questions return 400. Answer/confidence/score-level output columns are generated by the server. Get Action Field Options for `questions` returns templates and plain-language drafts.\n- **Object keyed by input reference**: `call_shortcut`'s `inputs` field — `{ \"<input reference>\": \"<value>\" }`, one entry per called-shortcut input you map (references come from Get Shortcut's `input_schema`). Values are strings with the same `{{public.ref}}` syntax.\n- **Array of condition groups**: for `path_filter` fields (filter action's `path_conditions`). Each group is `{ conditions: [{variable, operator, values?, combinator?}], combinator? }`. `variable` is a public reference token; `operator` is the comparison operator; `combinator` (AND/OR) joins the previous group at the group level and the previous leaf at the leaf level. See `/docs/action-detail/filter.txt` for the full body shape.\n\n**`label` defaulting on `jsonArray` items.** When an item carries a `name` but no `label`, the server stores `label: <name>` automatically, matching the `{ name, label, value }` triple the runtime engine consumes. Caller-supplied `label` values are always preserved, never overwritten.\n\n**Schema validation note.** This map's value type is intentionally permissive (`additionalProperties: true`). Fastify's Ajv strict mode reliably rejects nested `anyOf` array-of-object schemas under `additionalProperties` (it returns a confusing \"must be array\" error against values that *are* arrays). The six shapes above are enforced at the handler instead, which inspects each value's runtime type and routes it through the right storage branch.","additionalProperties":true,"example":{"linkedin_url":"{{input.linkedin_url}}","prompt":"Research {{input.company_name}} at {{enrich_company_linkedin_profile_1.website}}"}},"run_if":{"type":["object","null"],"description":"Optional gate deciding whether this action runs for a given row. Pass `null` to clear an existing condition. Omit to leave the existing condition unchanged.\n\n`conditions` is an array of AND/OR condition GROUPS — the same shape as the filter action's `path_conditions`. The outer array is groups, joined by each group's `combinator` (AND/OR); each group's `conditions` are leaf conditions, joined by each leaf's `combinator`. The first group's and first leaf's `combinator` is ignored. A single condition is just one group with one leaf.\n\n**Supported `operator` values depend on the variable's stored type** — the type is derived from the sheet's chain, so you don't pass it.\n\n- **Universal** (any type): `is`, `is not`, `is empty`, `is not empty`. `is empty` / `is not empty` ignore `values`.\n- **String / email / url / imageUrl**: `contains`, `does not contain`, `starts with`, `does not start with`, `ends with`, `does not end with`. Matching is case-insensitive.\n- **Number / currency**: `greater than`, `less than`, `greater than or equal to`, `less than or equal to`, `is between` (pass `[min, max]`).\n- **Date**: `is after`, `is before`, `is between` (pass `[start, end]`). Values are parsed by `moment(...)`.\n- **Array / structured_array / json**: `contains`, `does not contain`.\n\nPass `values` as an array of strings — number / date matchers coerce as needed.","properties":{"conditions":{"type":"array","description":"AND/OR condition groups. Outer array = groups (joined by each group's `combinator`); each group's `conditions` = leaf conditions (joined by each leaf's `combinator`). A single condition is one group with one leaf.","items":{"type":"object","properties":{"conditions":{"type":"array","items":{"type":"object","properties":{"variable":{"type":"string","description":"Reference token whose resolved value gets compared. Use `{{input.<field_name>}}` for a sheet input, or `{{<action_instance_id>.<field_name>}}` for an upstream action's output.","examples":["{{input.country}}","{{enrich_company_linkedin_profile_1.employee_count}}"]},"operator":{"type":"string","description":"Comparison operator. See the per-type list above.","examples":["is","greater than","is not empty","contains"]},"values":{"type":"array","description":"Values to compare against. Required for most operators; ignored by `is empty` / `is not empty`. For `is between`, pass `[low, high]`.","items":{},"examples":[["US","CA"],["100"]]},"combinator":{"type":"string","enum":["AND","OR"],"description":"Joins this leaf with the previous one in the group. Ignored on the first leaf."}},"required":["variable","operator"]}},"combinator":{"type":"string","enum":["AND","OR"],"description":"Joins this group with the previous one. Ignored on the first group."}},"required":["conditions"]}},"continue_workflow_if_run_condition_not_met":{"type":"boolean","description":"When `true`, downstream actions still run for a row even when this action's condition isn't met (the action itself is skipped). When `false` (default), the row stops at this action when the condition isn't met."}}},"continue_workflow_if_action_fails":{"type":["boolean","null"],"description":"When `true`, downstream actions still run for a row even if this action fails. When `false`, the row stops at this action on failure and downstream cells stay queued. Send `null` to clear the action's own setting so it follows the sheet's `continue_on_fail` (default off). Omit to leave it unchanged. Absent in responses means the action follows the sheet."},"note":{"type":"string","description":"Optional free-text note to save on the action. Same upsert behaviour as Save Action Note — overwrites any existing note. Whitespace-only values are ignored.","example":"Skip until Phase 2 — vendor still negotiating contract terms."},"output_types":{"type":"object","description":"Optional. Sets the declared type of outputs whose type is a per-action choice — today Formula's `result`. Map of output name (as Get Outputs returns it) → type: `string` (default), `number`, `boolean`, `date`, `currency`, `email`, `url`, `imageUrl` or `json`. The stored value is unchanged; the type decides which `run_if` / `filter` operators apply, and Get Outputs reports it. A condition takes its type when the condition is saved, so set the type before adding conditions on this output. Any other output name returns 400.","additionalProperties":{"type":"string","enum":["string","number","boolean","date","currency","email","url","imageUrl","json"]},"example":{"result":"number"}}},"example":{"inputs":{"linkedin_url":"{{input.linkedin_url}}"},"run_if":{"conditions":[{"conditions":[{"variable":"{{input.country}}","operator":"is","values":["US","CA"]}]}],"continue_workflow_if_run_condition_not_met":false},"continue_workflow_if_action_fails":false}},"ConfigureActionWarning":{"type":"object","description":"A non-fatal issue detected while applying a Configure Action request.","properties":{"field":{"type":"string","description":"The field name in the request body that this warning refers to.","example":"model"},"code":{"type":"string","description":"Machine-readable warning code. Known values: `unknown_field` (field not in the action's template, or a top-level body key the endpoint does not read, which is ignored; on a waterfall field also an unrecognised or duplicated provider name, or an array that produced no usable providers — the stored waterfall is then left unchanged), `unresolved_reference` (a `{{...}}` reference does not match any known variable on this sheet), `deprecated_format` (the request used a deprecated-but-still-accepted shape, e.g. the legacy flat `run_if` form — migrate to the documented shape), `ambiguous_provider` (a waterfall token matches one provider's display name and another's legacy id; resolved by name), `unsupported_own_key` (`use_own_key: true` on a provider that has no bring-your-own-key mode; ignored, the provider stays on Floqer credits), `own_key_required` (a provider with no Floqer-billed mode was configured without `use_own_key: true` and will fail at run time), `own_key_ignored` (Reorder Waterfall Providers only: `use_own_key` in the body disagreed with stored state — that endpoint reorders and never changes key state). `env_variable_unavailable` (a referenced environment variable is `workflow` scoped and has no value on this workflow — the reference is saved, but rows error until a value is set). `env_not_supported_in_conditions` (an environment variable was referenced in `run_if` or a Filter action's `path_conditions`; environment variables are substituted after conditions are evaluated, so that condition was skipped rather than saved). `missing_required_input` (a `call_shortcut` input the shortcut requires has no value and no default — rows fail before the shortcut runs until it is set). `broken_downstream_reference` (a `call_shortcut` action now calls a different shortcut, so its previous output columns are gone; the message names the actions that still reference them).","example":"unknown_field"},"message":{"type":"string","description":"Human-readable warning message suitable for display.","example":"Field 'model' is not recognized by this action and was ignored."}},"required":["field","code","message"]},"RenameActionBody":{"type":"object","description":"Body for Rename Action. Sets the action instance's display name.","properties":{"name":{"type":"string","description":"New display name for the action instance. Trimmed; cannot be empty after trimming.","minLength":1,"example":"Enrich primary contact"}},"required":["name"]},"ReorderWaterfallBody":{"type":"object","description":"Body for Reorder Waterfall Providers. Lists every currently configured provider by display name, exactly once, in the desired execution order.","properties":{"providers":{"type":"array","minItems":1,"items":{"anyOf":[{"type":"string","minLength":1},{"type":"object","properties":{"name":{"type":"string","minLength":1}},"required":["name"]}]},"description":"Ordered list of provider display names, matched case-insensitively (e.g. `Hunter`, `Lightning`, `DataSync`). Must match the set of providers currently configured on the field — reorder only, no adds or removes. Items may also be objects shaped `{ name }`, so the array Get Action returns in `inputs[<field>]` — where a provider on your own key comes back as `{ name, use_own_key: true }` — can be reordered and sent straight back. Key state is preserved from storage; `use_own_key` is ignored here, change it with Configure Action.","example":["Hunter","Prospeo","Lightning"]}},"required":["providers"]},"MoveActionBody":{"type":"object","description":"Body for Move Action. The moved action is placed immediately after the action identified by `after`.","properties":{"after":{"type":"string","minLength":1,"description":"An existing `action_instance_id` (NOT an `action_id`). Cannot equal the action being moved.","example":"enrich_company_linkedin_profile_1"}},"required":["after"]},"ActionGraphNode":{"type":"object","description":"One node in the workflow's action graph. `data[0]` is always the synthetic input node (`action_id: \"input\"`); subsequent elements are actions in topological execution order.","properties":{"action_instance_id":{"type":"string","description":"Unique ID of this node within the sheet. The synthetic input node uses `\"input\"`; action nodes use their per-sheet instance ID.","example":"input"},"action_id":{"type":"string","description":"Action template identifier. The synthetic input node uses `\"input\"`; action nodes use their template ID.","example":"input"},"display_name":{"type":"string","description":"User-facing name of the node.","example":"Inputs"},"inputs":{"type":"object","description":"Configured input wiring for this action: map of field name → value (literal / reference / waterfall array). Unconfigured fields are absent. Absent entirely on the input node.","additionalProperties":true},"outputs":{"type":"array","items":{"$ref":"#/components/schemas/ActionOutput"},"description":"Fields this node produces. On the input node, one entry per configured input field. Absent on actions that produce no outputs."},"continue_workflow_if_action_fails":{"type":"boolean","description":"When `true`, downstream actions still run for a row even if this action fails. Mirrors the value set via Configure Action's `continue_workflow_if_action_fails`. Absent when the action has never had it configured."},"run_if":{"type":"object","description":"Conditional gate that decides whether this action runs for a given row. Mirrors the value set via Configure Action's `run_if`. Absent when no condition is configured. Always returned in the nested `{ conditions: [groups] }` form (a single condition is one group with one leaf), the same shape Configure Action accepts. `continue_workflow_if_run_condition_not_met` controls whether the chain proceeds when the condition isn't met.","properties":{"conditions":{"type":"array","description":"AND/OR condition groups, identical shape to Configure Action's `run_if.conditions`. Outer array = groups (joined by each group's `combinator`); each group's `conditions` = leaf conditions (joined by each leaf's `combinator`).","items":{"type":"object","properties":{"conditions":{"type":"array","items":{"type":"object","properties":{"variable":{"type":"string"},"operator":{"type":"string"},"values":{"type":"array","items":{}},"combinator":{"type":"string","enum":["AND","OR"]}},"required":["variable","operator","values"]}},"combinator":{"type":"string","enum":["AND","OR"]}},"required":["conditions"]}},"continue_workflow_if_run_condition_not_met":{"type":"boolean","description":"When `true`, downstream actions still run for a row even when this action's condition isn't met. Default `false`."}},"required":["conditions","continue_workflow_if_run_condition_not_met"]},"next":{"type":"array","items":{"type":"string"},"description":"Downstream `action_instance_id`s. `[]` is terminal. Multiple IDs represent a branch point (reserved — currently at most one ID is populated)."},"note":{"type":"string","description":"Free-text note attached to this action via Save Action Note (or Configure Action's `note` field). Absent when no note has been saved."}},"required":["action_instance_id","action_id","display_name","next"]},"GetOptionsBody":{"type":"object","description":"Body for Get Action Field Options. `context` carries any earlier cascading selections (e.g. `{ salesforce_object: \"Account\" }` for Salesforce field lookups). Resolvers without cascades accept an empty body.","properties":{"context":{"type":"object","description":"Optional context for cascading resolvers. Keys are snake-cased and match the parent field's public name. Resolvers accept multiple snake-cased spellings — calling without a required key returns 400 listing the accepted spellings.","additionalProperties":true}}},"OptionItem":{"type":"object","description":"One value the resolver returned. `value` is what to send back via Configure Action; `label` is the human-readable text. `extras` carries integration-specific metadata when present (Salesforce field metadata, HubSpot property type, etc.).","properties":{"value":{"type":"string","description":"The value to send back via Configure Action."},"label":{"type":"string","description":"Human-readable label for the option."},"extras":{"type":"object","description":"Per-integration metadata. Shape varies by integration — use it for richer rendering when needed.","additionalProperties":true}},"required":["value","label"]},"GetOptionsData":{"type":"object","description":"Response data for Get Action Field Options.","properties":{"options":{"type":"array","items":{"$ref":"#/components/schemas/OptionItem"},"description":"Available option values for the requested field."}},"required":["options"]},"SaveNoteBody":{"type":"object","description":"Body for Save Action Note. Overwrites any existing note on the action.","properties":{"note":{"type":"string","minLength":1,"description":"Free-text note to attach to the action. Trimmed; cannot be empty after trimming.","example":"Skip until Phase 2 — vendor still negotiating contract terms."}},"required":["note"]},"ActionNote":{"type":"object","description":"A note attached to an action instance.","properties":{"action_instance_id":{"type":"string","description":"Public `action_instance_id` the note is attached to.","example":"enrich_company_linkedin_profile_1"},"sheet_id":{"type":"string","description":"Sheet the note is scoped to."},"note":{"type":"string","description":"The saved note text."}},"required":["action_instance_id","sheet_id","note"]},"RowWarning":{"type":"object","description":"A non-fatal issue detected while processing a rows batch.","properties":{"field":{"type":"string","description":"The field this warning refers to (input field name, or `_row` for row-level issues).","example":"email"},"code":{"type":"string","description":"Machine-readable warning code. Examples: `duplicate_row`, `value_normalized`.","example":"duplicate_row"},"message":{"type":"string","description":"Human-readable warning message."}},"required":["field","code","message"]},"RejectedRow":{"type":"object","description":"A row the server couldn't accept. Original content is echoed back so the caller can fix and resend without position tracking.","properties":{"row":{"type":"object","additionalProperties":true,"description":"The original row content as sent — echoed back exactly."},"errors":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","description":"The field that failed validation, or `_row` for row-level errors.","example":"email"},"code":{"type":"string","description":"Machine-readable failure code. Known values include `malformed_row`, `unknown_field`.","example":"malformed_row"},"message":{"type":"string","description":"Human-readable failure message."}},"required":["field","code","message"]}}},"required":["row","errors"]},"AddRowsBody":{"type":"object","description":"Body for Add Rows. Accepts up to 1,000 rows per call.","properties":{"rows":{"type":"array","maxItems":1000,"minItems":1,"items":{"type":"object","additionalProperties":true},"description":"Rows to add. Each row is a JSON object whose keys are input field names (as defined by Add Inputs) and whose values match the field's declared type. Max 1,000 per call."},"run_after_add":{"type":"string","enum":["none","first_10","all"],"description":"What to run after rows are written. Scoped to the rows in this call only — pre-existing rows on the sheet are never touched. `none` (default) — add rows, don't run. `first_10` — queue up to 10 of the newly-added rows for execution, in the order you sent them; recommended build-loop default. Batches with fewer than 10 rows queue every row in the batch. `all` — queue every newly-added row (⚠ full credit cost = N rows × actions in chain; avoid until output is verified on `first_10`)."}},"required":["rows"]},"AddRowsData":{"type":"object","description":"Outcome of an Add Rows batch.","properties":{"row_count":{"type":"integer","description":"Number of rows accepted and written. Equals `row_ids.length`."},"row_ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Flat array of newly-assigned row UUIDs, in request order. Rejected rows are not included. Copy directly into Run Rows's `row_ids` to execute."},"rejected":{"type":"array","items":{"$ref":"#/components/schemas/RejectedRow"},"description":"Rows the server couldn't accept (e.g. malformed shape, unknown fields). Valid rows in the same batch were still written — this list contains only the failures."},"rows_queued_for_run":{"type":"integer","description":"How many newly-added rows were queued for execution. `0` when `run_after_add: \"none\"`. `min(10, row_count)` when `\"first_10\"`. Equal to `row_count` when `\"all\"`."}},"required":["row_count","row_ids","rejected","rows_queued_for_run"]},"RunRowsBody":{"type":"object","description":"Body for Run Rows. Choose rows in exactly one way:\n- `row_ids` — an explicit list.\n- `first_10: true` — a shortcut.\n- `row_range` — rows `start` to `end` in List Rows order.\n- `filters` / `status_filters` / `created_at` — every row matching them, selected on the server (the same shapes List Rows takes). Use this to run a subset, e.g. rows whose cell on an action failed, without listing their ids first.\n\n`row_range` may be sent with the filters to run rows `start` to `end` of the filtered set. Otherwise none, or more than one, returns 400.","properties":{"row_ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Row UUIDs to queue through the action chain. Get these from Add Rows (`row_ids` response field) or List Rows. Rows on the sheet not included here are not re-run. Mutually exclusive with `first_10` and the filters.","example":["a1b2c3d4-e5f6-7890-abcd-ef1234567001","a1b2c3d4-e5f6-7890-abcd-ef1234567002","a1b2c3d4-e5f6-7890-abcd-ef1234567003"]},"first_10":{"type":"boolean","description":"Shortcut for \"run the first 10 rows on the sheet\": the 10 newest, i.e. the top of the table and page 1 of List Rows. Useful for experimentation and sampling on existing sheets. Mutually exclusive with `row_ids`. Pass `true` to enable; omit or pass `false` to use `row_ids` instead."},"row_range":{"type":"object","description":"Rows `start` to `end` (1-indexed, inclusive) in List Rows order, newest first — row 1 is the top of the table. With filters, the positions count within the filtered rows. At most 10,000 rows per call. An `end` past the last row runs up to the last row; a `start` past it returns 400.","properties":{"start":{"type":"integer","minimum":1,"description":"First row to run (1-indexed)."},"end":{"type":"integer","minimum":1,"description":"Last row to run (inclusive)."}},"required":["start","end"],"additionalProperties":false,"example":{"start":11,"end":50}},"filters":{"type":"array","description":"Value filters on sheet input columns. Each entry has `variable` (an `{{input.<column>}}` reference), `operator` (one of `is equal to`, `not equal to`, `is empty`, `not empty`, `contains`, `does not contain`, `contains any of`, `not contains any of`, `greater than`, `less than`), and `values` (array of strings — single value for most operators; multiple for `contains any of` / `not contains any of`; empty for `is empty` / `not empty`).","items":{"type":"object","properties":{"variable":{"type":"string","description":"Public reference token for the input column to filter on. Must match `{{input.<column>}}`.","example":"{{input.email}}"},"operator":{"type":"string","description":"Comparison operator. See the description above for the full list.","example":"contains"},"values":{"type":"array","items":{"type":"string"},"description":"Values to compare against. Single-value operators take one item; `contains any of` / `not contains any of` take one or more; `is empty` / `not empty` ignore values.","example":[".com"]}},"required":["variable","operator"]}},"status_filters":{"type":"array","description":"Per-action cell-status filters. Each entry specifies an `action_instance_id` and exactly one of `is` (status matches one of) or `is_not` (status matches none of). Status values use the public vocabulary (`queued | running | complete | failed | error | out_of_credits | condition_not_met`).","items":{"type":"object","properties":{"action_instance_id":{"type":"string","description":"Action instance ID (public form) whose per-row cell status to filter on.","example":"enrich_company_linkedin_profile_1"},"is":{"type":"array","items":{"type":"string"},"description":"Match rows whose cell on this action is in one of these statuses.","example":["complete"]},"is_not":{"type":"array","items":{"type":"string"},"description":"Match rows whose cell on this action is NOT in any of these statuses.","example":["failed","error"]}},"required":["action_instance_id"]}},"created_at":{"type":"object","description":"Filter rows by creation timestamp. `operator` is one of `equals to`, `greater than`, `greater than or equals to`, `less than`, `less than or equals to`, `is between`. `values` is `[ISO timestamp]` for single-value operators or `[start, end]` for `is between`.","properties":{"operator":{"type":"string","example":"greater than"},"values":{"type":"array","items":{"type":"string"},"example":["2026-05-07T22:14:00Z"]}},"required":["operator","values"]}}},"RunRowsData":{"type":"object","description":"Outcome of a Run Rows call.","properties":{"rows_queued":{"type":"integer","description":"Number of rows queued for execution. Equals `row_ids.length` when `row_ids` was provided, or 10 (or fewer if the sheet has fewer rows) when `first_10: true` was provided, or the rows found in `row_range`."}},"required":["rows_queued"]},"RowCell":{"type":"object","description":"One cell of a row (a single action × a single row). Branch on `status` to decide which of `outputs` / `outputs_ref` / `error` to read.","properties":{"status":{"type":"string","enum":["queued","running","complete","failed","error","out_of_credits","condition_not_met"],"description":"Lifecycle state of this cell. Progresses `queued → running → (complete | failed | error | out_of_credits | condition_not_met)`. Everything after `running` is terminal — stop polling the cell once it lands on one. `failed` is a known failure path (provider 404, validation failed, duplicate row, missing required input, run stopped); `error` is an unexpected runtime error; `out_of_credits` means the org's credit balance couldn't cover the action, so it never ran — top up and re-run the row. All three carry an `error` message. `condition_not_met` is terminal too — the action's `run_if` (or an upstream `filter`) evaluated false for this row, so the cell never ran; downstream cells stay `queued` unless `continue_workflow_if_run_condition_not_met: true` was set on the gating action."},"outputs":{"type":"object","additionalProperties":true,"description":"Inline outputs for a `complete` cell when the payload is small. Keys are the action's output field names. Absent on non-`complete` cells; absent on `complete` cells with `outputs_ref`."},"outputs_ref":{"type":"object","description":"URL pointer to the outputs of a `complete` cell when the payload is too large to inline (e.g. LinkedIn profile scrapes, enrichment blobs). Fetch the URL with the caller's API key to read the data — it returns the same `outputs` shape this cell would have carried inline.","properties":{"url":{"type":"string","format":"uri","description":"Absolute URL of the Get Cell Outputs endpoint for this cell. Authorized by the caller's API key like any other endpoint — it never expires and carries no credential of its own, so revoking the key revokes access. Payload fetches are metered in their own quota rather than against the general API rate limit."},"size_bytes":{"type":"integer","description":"Size of the outputs JSON at `url`, in bytes. Only present when the size is known without reading the payload; read `Content-Length` on the fetch otherwise."}},"required":["url"]},"error":{"type":"string","description":"Human-readable failure message. Present on `failed`, `error` and `out_of_credits` cells."}},"required":["status"]},"RowView":{"type":"object","description":"A row on a sheet with its inputs and per-cell action status/outputs.","properties":{"row_id":{"type":"string","format":"uuid","description":"UUID assigned by Add Rows."},"row_status":{"type":"string","enum":["pending","running","complete","has_failures"],"description":"Row-level execution summary — a quick check without iterating every cell. `pending`: the row has never been run (`cells` is `{}`). `running`: at least one cell is queued or running. `complete`: all cells reached `complete`. `has_failures`: terminal state — no cell is still running and at least one cell is `failed`, `error` or `out_of_credits`. Read `cells[<action_instance_id>].status` for the reason: `out_of_credits` clears by topping up credits and re-running, the others do not."},"created_at":{"type":"string","format":"date-time","description":"UTC timestamp (ISO 8601) when the row was added to the sheet via Add Rows."},"inputs":{"type":"object","additionalProperties":true,"description":"Input column values as the row was created. Keys match the sheet's input field names."},"cells":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/RowCell"},"description":"One entry per action in the sheet's chain, keyed by `action_instance_id` (e.g. `enrich_company_linkedin_profile_1`). Empty `{}` for rows that have never been run."}},"required":["row_id","row_status","created_at","inputs","cells"]},"ListRowsBody":{"type":"object","description":"Body for List Rows. All fields optional.\n\n**Filtering** (browse mode only — `filters` / `status_filters` / `created_at` cannot combine with `row_ids`; the server returns 400 if you mix them):\n- `filters[]` — value filters on **sheet input columns**. Each entry: `{ variable, operator, values }`. `variable` is an `{{input.<column>}}` reference token.\n- `status_filters[]` — per-action cell-status filters. Each entry: `{ action_instance_id, is?, is_not? }` (exactly one of `is` / `is_not`).\n- `created_at` — single time filter on the row's creation timestamp. `{ operator, values }`.","properties":{"row_ids":{"type":"array","maxItems":200,"items":{"type":"string","format":"uuid"},"description":"Filter to specific rows. Omit to return all rows on the sheet (paginated). Max 200 IDs per call — if you have more than 200 to poll, chunk your IDs across multiple calls. Mutually exclusive with `filters` / `status_filters` / `created_at`."},"page_no":{"type":"integer","minimum":1,"description":"Page number (1-indexed). Only meaningful when browsing (no `row_ids`) or when `row_ids` count exceeds `page_size`."},"page_size":{"type":"integer","minimum":1,"maximum":200,"description":"Rows per page. Defaults to 20. Maximum 200."},"filters":{"type":"array","description":"Value filters on sheet input columns. Each entry has `variable` (an `{{input.<column>}}` reference), `operator` (one of `is equal to`, `not equal to`, `is empty`, `not empty`, `contains`, `does not contain`, `contains any of`, `not contains any of`, `greater than`, `less than`), and `values` (array of strings — single value for most operators; multiple for `contains any of` / `not contains any of`; empty for `is empty` / `not empty`).","items":{"type":"object","properties":{"variable":{"type":"string","description":"Public reference token for the input column to filter on. Must match `{{input.<column>}}`.","example":"{{input.email}}"},"operator":{"type":"string","description":"Comparison operator. See the description above for the full list.","example":"contains"},"values":{"type":"array","items":{"type":"string"},"description":"Values to compare against. Single-value operators take one item; `contains any of` / `not contains any of` take one or more; `is empty` / `not empty` ignore values.","example":[".com"]}},"required":["variable","operator"]}},"status_filters":{"type":"array","description":"Per-action cell-status filters. Each entry specifies an `action_instance_id` and exactly one of `is` (status matches one of) or `is_not` (status matches none of). Status values use the public vocabulary (`queued | running | complete | failed | error | out_of_credits | condition_not_met`).","items":{"type":"object","properties":{"action_instance_id":{"type":"string","description":"Action instance ID (public form) whose per-row cell status to filter on.","example":"enrich_company_linkedin_profile_1"},"is":{"type":"array","items":{"type":"string"},"description":"Match rows whose cell on this action is in one of these statuses.","example":["complete"]},"is_not":{"type":"array","items":{"type":"string"},"description":"Match rows whose cell on this action is NOT in any of these statuses.","example":["failed","error"]}},"required":["action_instance_id"]}},"created_at":{"type":"object","description":"Filter rows by creation timestamp. `operator` is one of `equals to`, `greater than`, `greater than or equals to`, `less than`, `less than or equals to`, `is between`. `values` is `[ISO timestamp]` for single-value operators or `[start, end]` for `is between`.","properties":{"operator":{"type":"string","example":"greater than"},"values":{"type":"array","items":{"type":"string"},"example":["2026-05-07T22:14:00Z"]}},"required":["operator","values"]}}},"ListRowsData":{"type":"object","description":"Paginated list of rows with their inputs and per-cell status.","properties":{"rows":{"type":"array","items":{"$ref":"#/components/schemas/RowView"},"description":"Rows on this page, newest first (by `created_at` descending)."},"total_count":{"type":"integer","description":"Total rows being paginated. In browse mode (no `row_ids`): the sheet-wide row count. In filter mode (`row_ids` provided): the size of the `row_ids` filter — the number of IDs the caller asked about, not the sheet's total."},"page_no":{"type":"integer"},"page_size":{"type":"integer"}},"required":["rows","total_count","page_no","page_size"]},"DeleteRowsBody":{"type":"object","description":"Body for Delete Rows. Accepts up to 200 row UUIDs per call.","properties":{"row_ids":{"type":"array","maxItems":200,"minItems":1,"items":{"type":"string"},"description":"Row UUIDs to delete. From Add Rows (`row_ids` response) or List Rows. Max 200 per call."}},"required":["row_ids"]},"DeletedRowRejection":{"type":"object","description":"A row UUID the server couldn't delete, with a human-readable reason.","properties":{"row_id":{"type":"string","description":"The UUID you sent that couldn't be deleted."},"error":{"type":"string","description":"Human-readable reason (e.g. `Row not found on this sheet`)."}},"required":["row_id","error"]},"DeleteRowsData":{"type":"object","description":"Outcome of a Delete Rows batch.","properties":{"deleted_count":{"type":"integer","description":"Number of rows actually deleted. Equals `deleted_row_ids.length`."},"deleted_row_ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"UUIDs that were successfully deleted, in request order (skipping any rejected)."},"rejected":{"type":"array","items":{"$ref":"#/components/schemas/DeletedRowRejection"},"description":"Row UUIDs the server couldn't delete. Rows that WERE deleted in the same batch are still gone — this list contains only the failures."}},"required":["deleted_count","deleted_row_ids","rejected"]},"DeleteAllRowsAccepted":{"type":"object","description":"Acceptance of a Delete All Rows call. The delete runs in the background; poll Get Delete with `job_id`.","properties":{"status":{"type":"string","enum":["processing"],"description":"Always `processing` on acceptance."},"job_id":{"type":"string","description":"ID of the delete. Pass it to Get Delete to poll. Forgotten after 24 hours."}},"required":["status","job_id"]},"DeleteJobData":{"type":"object","description":"Progress of a delete started by Delete All Rows.","properties":{"job_id":{"type":"string","description":"ID of the delete."},"status":{"type":"string","enum":["processing","completed","failed"],"description":"`processing` — rows still being removed. `completed` — finished. `failed` — see `error`; deleting again finishes the job."},"deleted_count":{"type":"integer","description":"Rows removed so far. Final once `status` is `completed`: the rows that actually matched at the time each was reached, not an estimate."},"error":{"type":"string","description":"Why the delete failed. Only on `failed`."}},"required":["job_id","status","deleted_count"]},"ImportCsvBody":{"type":"object","description":"Body for Import CSV. The file must already be uploaded to Floqer; pass its object key.","properties":{"file_ref":{"type":"string","description":"Object key of the already-uploaded file, exactly as Floqer issued it. Not a URL — a URL is rejected. The file must have been uploaded by someone in your organization.","example":"leads.csv_jane@acme.com_9f3c1a2b-4d5e-6f70-8a9b-0c1d2e3f4a5b.csv"},"mapping":{"type":"object","additionalProperties":{"type":"string"},"description":"Column header in the file -> input field name on the sheet. Field names come from List Inputs and must match exactly (case- and whitespace-sensitive). Headers are read from row 1; naming a header the file does not have fails the import and reports which one. Columns omitted here are not imported; input fields with no column are left empty. Two columns may not map to the same field.","example":{"Email":"email","Company Name":"company_name","LinkedIn URL":"linkedin_url"}},"sheet_name":{"type":"string","description":"XLSX only — which worksheet to import. Defaults to the first sheet in the workbook. Naming a sheet the workbook does not have fails the import."},"run_after_import":{"type":"string","enum":["none","first_10","all"],"description":"What to run once the rows are imported. Scoped to the imported rows only — pre-existing rows on the sheet are never touched. `none` (default) — import, don't run. `first_10` — queue up to 10 of the imported rows; recommended before committing to a full file. `all` — queue every imported row (WARNING: full credit cost = N rows x actions in chain; verify output on `first_10` first)."}},"required":["file_ref","mapping"]},"ImportCsvData":{"type":"object","description":"Acknowledgement that the import was accepted and queued. Poll Get CSV Import Status for the outcome.","properties":{"status":{"type":"string","enum":["processing"],"description":"Always `processing` — the import runs in the background."},"sheet_id":{"type":"string","description":"Sheet the rows will land on."},"columns_mapped":{"type":"integer","description":"How many of the file's columns will be imported — the number of entries in `mapping`."}},"required":["status","sheet_id","columns_mapped"]},"ImportCsvStatusData":{"type":"object","description":"Progress of the sheet's most recent CSV import.","properties":{"status":{"type":"string","enum":["processing","completed","failed","unknown"],"description":"`processing` — rows still arriving. `completed` — finished; `rows_imported` is final. `failed` — see `error`. `unknown` — no recent import status is held for this sheet."},"rows_imported":{"type":"integer","description":"Rows written so far. Final once `status` is `completed`."},"error":{"type":"string","description":"Why the import failed. A header named in `mapping` but absent from the file is reported here, e.g. `header \"Email\" not found in file`."}},"required":["status","rows_imported"]},"ExportSheetBody":{"type":"object","description":"Body for Export Sheet. All fields optional — omit the body entirely to export every row on the sheet.\n\n**Choosing rows** — either name them explicitly with `row_ids`, or narrow with `filters` / `status_filters` / `created_at` (the same shapes List Rows takes). The two cannot combine; the server returns 400 if you mix them. `exclude_row_ids` subtracts from the filtered set.\n\nThe file is always CSV.","properties":{"row_ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Export exactly these rows. Omit to export every row matching the filters (or the whole sheet). Mutually exclusive with `filters` / `status_filters` / `created_at`. Ids that are not on the sheet are dropped, so `row_count` can be lower than the number sent; a selection that matches nothing is a 400."},"exclude_row_ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Rows to leave out of the exported set. Applied after the filters; ignored when `row_ids` is given, since that is already an explicit list."},"filters":{"type":"array","description":"Value filters on sheet input columns. Each entry has `variable` (an `{{input.<column>}}` reference), `operator` (one of `is equal to`, `not equal to`, `is empty`, `not empty`, `contains`, `does not contain`, `contains any of`, `not contains any of`, `greater than`, `less than`), and `values` (array of strings — single value for most operators; multiple for `contains any of` / `not contains any of`; empty for `is empty` / `not empty`).","items":{"type":"object","properties":{"variable":{"type":"string","description":"Public reference token for the input column to filter on. Must match `{{input.<column>}}`.","example":"{{input.email}}"},"operator":{"type":"string","description":"Comparison operator. See the description above for the full list.","example":"contains"},"values":{"type":"array","items":{"type":"string"},"description":"Values to compare against. Single-value operators take one item; `contains any of` / `not contains any of` take one or more; `is empty` / `not empty` ignore values.","example":[".com"]}},"required":["variable","operator"]}},"status_filters":{"type":"array","description":"Per-action cell-status filters. Each entry specifies an `action_instance_id` and exactly one of `is` (status matches one of) or `is_not` (status matches none of). Status values use the public vocabulary (`queued | running | complete | failed | error | out_of_credits | condition_not_met`).","items":{"type":"object","properties":{"action_instance_id":{"type":"string","description":"Action instance ID (public form) whose per-row cell status to filter on.","example":"enrich_company_linkedin_profile_1"},"is":{"type":"array","items":{"type":"string"},"description":"Match rows whose cell on this action is in one of these statuses.","example":["complete"]},"is_not":{"type":"array","items":{"type":"string"},"description":"Match rows whose cell on this action is NOT in any of these statuses.","example":["failed","error"]}},"required":["action_instance_id"]}},"created_at":{"type":"object","description":"Filter rows by creation timestamp. `operator` is one of `equals to`, `greater than`, `greater than or equals to`, `less than`, `less than or equals to`, `is between`. `values` is `[ISO timestamp]` for single-value operators or `[start, end]` for `is between`.","properties":{"operator":{"type":"string","example":"greater than"},"values":{"type":"array","items":{"type":"string"},"example":["2026-05-07T22:14:00Z"]}},"required":["operator","values"]},"file_name":{"type":"string","description":"Base name for the downloaded file, without extension. Defaults to `export`. Non-alphanumeric characters are replaced and the name is capped at 100 characters.","example":"qualified-leads"}}},"ExportJob":{"type":"object","description":"An export job. Poll Get Export until `status` is `completed` or `failed`.","properties":{"export_id":{"type":"string","description":"ID of this export. Pass it to Get Export to poll. Valid for 24 hours, after which the job is forgotten."},"status":{"type":"string","enum":["running","completed","failed"],"description":"`running` — queued or building. `completed` — `file_url` is ready. `failed` — see `error`. There is no separate queued state; a job is `running` from the moment it is accepted."},"progress":{"type":"integer","description":"Percent complete, 0-100. Reaches 100 only when `status` is `completed`."},"row_count":{"type":"integer","description":"Rows the export will contain, resolved against the sheet when the job was accepted — a count of rows that actually matched, never an echo of how many `row_ids` were sent."},"file_url":{"type":"string","description":"Time-limited download link, present only when `status` is `completed`. Minted fresh on every poll and valid for 15 minutes — it is a signed URL, so treat it as a secret and do not log or forward it. Re-poll for a new one after it expires."},"expires_at":{"type":"string","format":"date-time","description":"When `file_url` stops working (UTC, ISO 8601). Present only alongside `file_url`."},"error":{"type":"string","description":"Why the export failed. Present only when `status` is `failed`."},"created":{"type":"boolean","description":"Export Sheet only. `true` when this call started the job; `false` when an export was already building for you on this sheet and you were handed that one instead."},"started_at":{"type":"string","format":"date-time","description":"When the job was accepted (UTC, ISO 8601)."},"completed_at":{"type":["null","string"],"format":"date-time","description":"When the job reached a terminal state (UTC, ISO 8601). `null` while still running."}},"required":["export_id","status","progress","row_count","started_at","completed_at"]},"EnvironmentVariable":{"type":"object","description":"An organization environment variable. Reference it from an action field with the `reference` token; the value is substituted per row when the workflow runs. Values are never returned by the API.","properties":{"key":{"type":"string","description":"Canonical identifier, unique within the organization and immutable once created. This is the segment used in the reference token."},"name":{"type":"string","description":"Display name shown in the Floqer app. Editable, so never reference a variable by it."},"description":{"type":["null","string"],"description":"Optional free-text description set by the org. `null` when unset."},"scope":{"type":"string","enum":["org","workflow","user"],"description":"Which axis the value varies along. `org` — one value for the whole organization. `workflow` — one value per workflow, so the variable only resolves in workflows that have one set. `user` — one value per member, resolved against the workflow's OWNER (not whoever triggers the run)."},"reference":{"type":"string","description":"Ready-to-paste reference token. Use it as (or inside) any string value in Configure Action's `inputs`.","example":"{{env.api_base}}"},"value_count":{"type":"integer","description":"How many scope points currently have a value: 1 or 0 for `org` scope, and for `workflow` / `user` scope the number of workflows / members with one set. `0` means the variable resolves nowhere yet."},"created_by":{"type":["null","string"],"description":"Email of the member who created the variable. `null` on older rows."},"updated_at":{"type":"string","format":"date-time","description":"UTC timestamp (ISO 8601) the definition was last changed. Setting a value does not change it."}},"required":["key","name","description","scope","reference","value_count","created_by","updated_at"]},"UsageDailyItem":{"type":"object","description":"One UTC day of activity.","properties":{"date":{"type":"string","description":"The day, `YYYY-MM-DD` in UTC."},"runs":{"type":"integer","description":"Action runs that day."},"credits":{"type":"number","description":"Credits consumed that day. May be fractional."},"cache_hits":{"type":"integer","description":"Runs served from cache that day. These cost no credits and are included in `runs`."}},"required":["date","runs","credits","cache_hits"]},"UsageTopAction":{"type":"object","description":"An action's share of the usage, ranked by credits.","properties":{"action_name":{"type":"string","description":"Display name of the action, e.g. `Find Email`. Falls back to the internal slug when unmapped."},"runs":{"type":"integer"},"credits":{"type":"number"},"cache_hits":{"type":"integer","description":"How many of `runs` were served from cache."}},"required":["action_name","runs","credits"]},"UsageTopWorkflow":{"type":"object","description":"A workflow's share of the usage, ranked by credits.","properties":{"sheet_id":{"type":"string","description":"UUID of the SHEET the runs happened on. Pass to `GET /api/v1/usage/workflows/{sheet_id}`, or look it up in List Workflow Usage for the workflow it belongs to."},"name":{"type":"string"},"deleted":{"type":"boolean","description":"True when the workflow has since been deleted. Its usage still counted, so it is still listed."},"shortcut_name":{"type":["null","string"],"description":"Name of the shortcut this sheet backs, with `(archived)` appended when that shortcut is archived; null when it backs none. The sheet's own names are unchanged. A run of a global catalog shortcut has no workflow, so it carries the shortcut's name here and in `name`."},"runs":{"type":"integer"},"credits":{"type":"number"}},"required":["sheet_id","name","deleted","shortcut_name","runs","credits"]},"UsageTopUser":{"type":"object","description":"A member's share of the usage. Only populated for a key that can see the whole organization.","properties":{"email":{"type":"string"},"runs":{"type":"integer"},"credits":{"type":"number"}},"required":["email","runs","credits"]},"UsageCredits":{"type":"object","description":"The organization's spendable credit balance.","properties":{"credits":{"type":"number","description":"Credits available to spend right now. May be fractional, and may be negative if an adjustment overdrew the account."}},"required":["credits"]},"UsageOverview":{"type":"object","description":"Usage totals for the requested window, with the daily trend and the top consumers.","properties":{"summary":{"type":"object","properties":{"total_runs":{"type":"integer","description":"Action runs in the window."},"credits_consumed":{"type":"number","description":"Credits spent in the window."},"success_rate":{"type":"number","description":"Percentage of runs that succeeded, 0-100."},"cache_savings":{"type":"number","description":"Credits NOT spent because a run was served from cache. Not part of `credits_consumed`."},"avg_cost_per_run":{"type":"number","description":"`credits_consumed / total_runs`."},"active_users":{"type":"integer","description":"Members who ran something. Omitted for a key that only sees its own usage."}},"required":["total_runs","credits_consumed","success_rate","cache_savings","avg_cost_per_run"]},"daily":{"type":"array","items":{"$ref":"#/components/schemas/UsageDailyItem"}},"top_workflows":{"type":"array","items":{"$ref":"#/components/schemas/UsageTopWorkflow"}},"top_actions":{"type":"array","items":{"$ref":"#/components/schemas/UsageTopAction"}},"top_users":{"type":"array","items":{"$ref":"#/components/schemas/UsageTopUser"},"description":"Empty for a key that only sees its own usage."},"top_error":{"type":["null","object"],"description":"The most frequent error in the window, or null when nothing failed.","properties":{"message":{"type":"string"},"count":{"type":"integer"}}}},"required":["summary","daily","top_workflows","top_actions","top_users","top_error"]},"UsageWorkflowRow":{"type":"object","description":"Usage for one sheet. A workflow's sheets are separate rows sharing a `workflow_id` — sum them for the workflow total. Field names match the Sheet schema: `sheet_id` is this row, `workflow_id` is the workflow it belongs to.","properties":{"sheet_id":{"type":"string","description":"UUID of the sheet. Pass to Get Workflow Usage, and as `{sheet_id}` on every sheet-scoped workflow endpoint. Runs of a global catalog shortcut are listed under their run's own id, named after the shortcut; only the usage endpoints accept that id."},"name":{"type":"string","description":"Fully qualified name, `Parent > Sheet` for a child sheet."},"deleted":{"type":"boolean","description":"True when the workflow has since been deleted."},"workflow_id":{"type":"string","description":"UUID of the parent workflow this sheet belongs to — the `{workflow_id}` of the workflow endpoints. Equals `sheet_id` on a main sheet."},"workflow_name":{"type":"string","description":"The parent workflow's name."},"sheet_name":{"type":"string","description":"This sheet's own label — `Main Sheet` for an unnamed main sheet — without the parent prefix."},"shortcut_name":{"type":["null","string"],"description":"Name of the shortcut this sheet backs, with `(archived)` appended when that shortcut is archived; null when it backs none. The sheet's own names are unchanged. A run of a global catalog shortcut has no workflow, so it carries the shortcut's name here and in `name`."},"is_main_sheet":{"type":"boolean"},"runs":{"type":"integer"},"credits":{"type":"number"},"success_count":{"type":"integer","description":"Runs that succeeded."},"cache_hits":{"type":"integer"},"cache_hit_rate":{"type":"number","description":"Percentage of runs served from cache, 0-100."},"top_actions":{"type":"array","items":{"$ref":"#/components/schemas/UsageTopAction"},"description":"The three actions that spent the most credits in this sheet."}},"required":["sheet_id","name","deleted","workflow_id","workflow_name","sheet_name","shortcut_name","is_main_sheet","runs","credits","success_count","cache_hits","cache_hit_rate","top_actions"]},"UsageSourceRow":{"type":"object","description":"Usage for one source instance.","properties":{"source_instance_id":{"type":"string","description":"The source INSTANCE id, as listed by `GET /api/v1/sources`. Not the source-type slug."},"name":{"type":"string"},"deleted":{"type":"boolean","description":"True when the source has since been deleted."},"runs":{"type":"integer"},"credits":{"type":"number"},"success_count":{"type":"integer"},"cache_hits":{"type":"integer"},"cache_hit_rate":{"type":"number","description":"Percentage of runs served from cache, 0-100."},"top_actions":{"type":"array","items":{"$ref":"#/components/schemas/UsageTopAction"},"description":"The three actions that spent the most credits on this source."}},"required":["source_instance_id","name","deleted","runs","credits","success_count","cache_hits","cache_hit_rate","top_actions"]},"UsageWorkflowDetail":{"type":"object","description":"One sheet's usage in the window, broken down by day, action, error, run, and member.","properties":{"summary":{"type":"object","properties":{"sheet_id":{"type":"string"},"name":{"type":"string"},"deleted":{"type":"boolean"},"shortcut_name":{"type":["null","string"],"description":"Name of the shortcut this sheet backs, with `(archived)` appended when that shortcut is archived; null when it backs none. The sheet's own names are unchanged. A run of a global catalog shortcut has no workflow, so it carries the shortcut's name here and in `name`."},"total_runs":{"type":"integer"},"credits_consumed":{"type":"number"},"success_rate":{"type":"number","description":"Percentage, 0-100."},"cache_hit_rate":{"type":"number","description":"Percentage, 0-100."}},"required":["sheet_id","name","deleted","shortcut_name","total_runs","credits_consumed","success_rate","cache_hit_rate"]},"daily":{"type":"array","items":{"$ref":"#/components/schemas/UsageDailyItem"}},"top_actions":{"type":"array","items":{"$ref":"#/components/schemas/UsageTopAction"}},"top_errors":{"type":"array","description":"The most frequent errors in the window.","items":{"type":"object","properties":{"message":{"type":"string"},"count":{"type":"integer"},"last_seen":{"type":"string","description":"ISO 8601."}},"required":["message","count","last_seen"]}},"recent_runs":{"type":"array","description":"The 20 most recent runs.","items":{"type":"object","properties":{"run_id":{"type":"string","description":"Opaque identifier for the run. Use as a key only; the format is not part of the contract."},"created_at":{"type":"string","description":"ISO 8601."},"action_name":{"type":["null","string"]},"email":{"type":"string","description":"The member who ran it."},"credits":{"type":"number"},"status":{"type":["null","string"]},"fulfillment_source":{"type":"string","description":"How the run was answered: `api_call`, `cache_hit`, `user_api_key`, or `force_consumed`."}},"required":["run_id","created_at","action_name","email","credits","status","fulfillment_source"]}},"per_user":{"type":"array","items":{"$ref":"#/components/schemas/UsageTopUser"},"description":"Empty for a key that only sees its own usage."}},"required":["summary","daily","top_actions","top_errors","recent_runs","per_user"]},"SourcePreviewBody":{"type":"object","description":"Per-source preview body. Shape depends on `source_id` — see the provider README under `modules/sources/` (and optionally `/docs/source-detail/{source_id}.txt`). Validated at the handler level; the schema only enforces that the body is a JSON object.\n\nOne key is source-agnostic: `limit` (optional, positive integer) caps how many rows come back. It is consumed by the preview endpoint and never reaches the source, so it works on every source type and does not reduce upstream fetching or change `metadata.total_results`.","properties":{"limit":{"type":"integer","minimum":1,"description":"Optional cap on rows returned. Response-shaping only — the source still fetches its normal preview sample, `metadata.total_results` is unchanged, and `metadata.capped` becomes true when rows are dropped.","example":3}},"additionalProperties":true},"SourcePreviewResponse":{"type":"object","description":"Preview result envelope. `data` is capped per source (at most 100 rows — e.g. find_job_postings returns up to 25, Slack up to 10). `metadata.total_results` is the upstream count. Per-row keys depend on the source.","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":true},"description":"Preview rows. Each row's keys depend on the source's payload."},"metadata":{"type":"object","properties":{"total_results":{"type":"integer","description":"Total matching records in this preview response. Omitted when the source cannot count them."},"capped":{"type":"boolean","description":"When true, more rows exist upstream but were omitted due to preview limits. Omitted when not applicable."}}}},"required":["data","metadata"]},"SourceCreateBody":{"type":"object","description":"Per-source create body. Shape depends on `source_id` — see the provider README under `modules/sources/` (and optionally `/docs/source-detail/{source_id}.txt`). Validated at the handler level; the schema only enforces that the body is a JSON object.","additionalProperties":true},"SourceCreateResponse":{"type":"object","description":"Create result envelope. `source_instance_id` is the new source's UUID — pass it to Sync / Get Source Data / Pause-Resume.","properties":{"source_instance_id":{"type":"string","description":"UUID of the newly created source instance."},"name":{"type":"string","description":"Display name echoed from the request (trimmed)."},"created_at":{"type":"string","format":"date-time","description":"UTC timestamp (ISO 8601) of source creation."},"pixel_id":{"type":"string","description":"Website-visitor sources only: tracking pixel id. Embed `pixel_script` on the monitored domain so visits can be recorded."},"pixel_script":{"type":"string","description":"Website-visitor sources only: HTML snippet to embed on the monitored domain."},"warning":{"type":"string","description":"Non-fatal warning when the source was created but a secondary setup step failed (e.g. Google Sheets real-time sync could not be enabled)."}},"required":["source_instance_id","name","created_at"]},"SyncSourceBody":{"type":"object","description":"Body for Sync Source to Workflow. Maps workflow inputs to source fields and (by default) backfills existing rows.","properties":{"workflow_id":{"type":"string","format":"uuid","description":"UUID of the destination workflow."},"field_mapping":{"type":"object","additionalProperties":{"type":"string"},"description":"Map of workflow input reference → source field, e.g. `{ \"input.email\": \"email\", \"input.first_name\": \"firstname\" }`. Keys are `input.<field>` references from the workflow's inputs; values are source field names. Keys are CASE-SENSITIVE and must be the exact lowercase snake_case form returned by List Inputs (the input's `reference` without the `{{ }}` braces) — e.g. `input.first_name`, not `Input.First_Name` or `input.firstName`. Surrounding `{{ }}` is tolerated; case/spelling variants are not, and return 400 `unknown input`."},"push_existing":{"type":"boolean","description":"Whether to backfill the workflow with the source's current rows. Defaults to true."},"run":{"type":"string","deprecated":true,"description":"Deprecated and ignored (reported in `warnings[]`). Whether landed rows execute is the sheet's `auto_run` toggle — set it with Update Sheet before syncing. Will be rejected in a future release."}},"required":["workflow_id","field_mapping"],"additionalProperties":false},"SyncSourceResponse":{"type":"object","description":"Confirmation that the source is connected to the workflow.","properties":{"source_instance_id":{"type":"string","description":"The connected source's instance UUID."},"workflow_id":{"type":"string","description":"The destination workflow's UUID."},"push_existing":{"type":"boolean","description":"Whether existing rows were queued for backfill."},"fields_mapped":{"type":"integer","description":"Number of workflow inputs mapped."}},"required":["source_instance_id","workflow_id"]},"GetSourceOptionsBody":{"type":"object","description":"Body for Get Source Field Options. `context` carries any earlier cascading selections (e.g. `{ object_type: \"contacts\" }` for HubSpot property lookups). Resolvers without cascades accept an empty body.","properties":{"context":{"type":"object","description":"Optional context for cascading resolvers. Keys are snake-cased and match the parent field's public name on the source's payload. Resolvers accept multiple snake-cased spellings — calling without a required key returns 400 listing the accepted spellings. `hubspot_lists` also accepts an optional `search` key: words matched against list names (case-insensitive) so a list can be found by name instead of pulling the whole catalog.","additionalProperties":true}}},"SourceOptionItem":{"type":"object","description":"One value the resolver returned. `value` is what to send back in the source's create / preview payload; `label` is the human-readable text. `extras` carries integration-specific metadata when present (HubSpot property type, list color, etc.).","properties":{"value":{"type":"string","description":"The value to send back via the source's create / preview payload."},"label":{"type":"string","description":"Human-readable label for the option."},"extras":{"type":"object","description":"Per-integration metadata. Shape varies by integration — use it for richer rendering when needed.","additionalProperties":true}},"required":["value","label"]},"GetSourceOptionsData":{"type":"object","description":"Response data for Get Source Field Options.","properties":{"options":{"type":"array","items":{"$ref":"#/components/schemas/SourceOptionItem"},"description":"Available option values for the requested field."}},"required":["options"]},"SourceListItem":{"type":"object","description":"A source the caller has created. Only sources whose type this API supports are listed.","properties":{"source_instance_id":{"type":"string","description":"Source instance UUID — pass to `POST /api/v1/sources/{source_instance_id}/sync` or `GET /api/v1/sources/{source_instance_id}/data`."},"name":{"type":"string","description":"Display name given at creation."},"source_id":{"type":"string","description":"Public source-type slug (e.g. `import_from_hubspot`, `find_companies`) — the identifier used by preview / create / options."},"status":{"type":"string","description":"Source status: `active` (live, re-importing), `completed` (one-time import, won't refresh), `paused`, `paused_out_of_credits`, `expired`, or `deleted`."},"lead_count":{"type":"integer","nullable":true,"description":"Rows imported so far."},"created_at":{"type":"string","format":"date-time","description":"UTC creation timestamp."},"expiration_date":{"type":"string","nullable":true,"description":"When an active source stops firing."}},"required":["source_instance_id","name","source_id"]},"GetSourceDataResponse":{"type":"object","description":"Paginated rows imported into the source. `total_count` is the full row count in storage; `rows` is the current page.","properties":{"rows":{"type":"array","items":{"type":"object","additionalProperties":true},"description":"Imported rows for this page. Field names depend on the source type."},"total_count":{"type":"integer","description":"Total number of rows stored for this source."},"page_no":{"type":"integer","description":"1-indexed page number returned."},"page_size":{"type":"integer","description":"Page size used for this response."}},"required":["rows","total_count","page_no","page_size"]},"PatchSourceStatusBody":{"type":"object","description":"Lifecycle control for the source.","properties":{"status":{"type":"string","enum":["active","paused"],"description":"`paused` stops recurring runs; `active` resumes them."}},"required":["status"],"additionalProperties":false},"PatchSourceStatusResponse":{"type":"object","description":"Confirmation of the source's new public lifecycle status.","properties":{"source_instance_id":{"type":"string","description":"The source instance UUID."},"status":{"type":"string","enum":["active","paused"],"description":"The source's new public status (`active` or `paused`)."}},"required":["source_instance_id","status"]},"InputField":{"type":"object","description":"Definition of a single input field in a shortcut's input schema. Each field maps to a column in the underlying workflow's input action.","properties":{"reference":{"type":"string","description":"Snake_case identifier for this input field (derived from `name`) — use it as the key in `input_data` when running the shortcut","example":"linkedin_url"},"name":{"type":"string","description":"Human-readable field name displayed in the shortcut UI","example":"LinkedIn URL"},"description":{"type":"string","description":"Help text describing what value to provide for this field","example":"Full LinkedIn company profile URL"},"type":{"type":"string","description":"Field type: text, number, email, url, etc.","example":"text"},"required":{"type":"boolean","description":"Whether this field must be provided when running the shortcut","example":true},"defaultValue":{"description":"Default value used when the field is not provided"}},"additionalProperties":true},"OutputConfig":{"type":"object","description":"Definition of a single output field in a shortcut's output schema. Each field maps to a column produced by the workflow's OUTPUT_CUSTOM_ENRICHMENT action.","properties":{"name":{"type":"string","description":"Human-readable name for this output field — the key for its value in a run's `output_data`","example":"Company Website"},"description":{"type":"string","description":"Description of what this output contains","example":"Main website URL of the company"},"display_column":{"type":"string","description":"Column header label shown in results tables","example":"Company Website"}},"additionalProperties":true},"Shortcut":{"type":"object","description":"A shortcut wraps a workflow with typed input/output schemas, providing a simplified interface for running workflows programmatically. Shortcuts can be shared across an organization.","properties":{"id":{"type":"string","description":"Unique shortcut identifier (UUID)","example":"d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a"},"workflow_id":{"type":"string","description":"UUID of the underlying workflow that powers this shortcut","example":"531952c2-3d07-4be2-8c4b-733acba3187b"},"parent_app_id":{"type":"string","nullable":true,"description":"If this is a shared copy, the ID of the original master shortcut. Null for master shortcuts."},"floqer_catalog_id":{"type":"string","description":"Global identity when this shortcut is shared across organizations."},"call_shortcut_id":{"type":"string","description":"Source shortcut ID accepted by a Call Shortcut workflow action."},"floqer_source_version":{"type":"integer","description":"Current published source version for a global shortcut."},"is_virtual":{"type":"boolean","description":"Whether this shortcut is a read-only projection."},"name":{"type":"string","description":"Shortcut display name","example":"Company Enrichment"},"description":{"type":"string","description":"Human-readable description of what the shortcut does","example":"Enriches company data from a LinkedIn URL"},"creator_email":{"type":"string","description":"Email of the user who created the shortcut","example":"user@company.com"},"org_id":{"type":"string","description":"Organization ID the shortcut belongs to"},"visibility":{"type":"string","nullable":true,"description":"Get Shortcut only. `org`: everyone in the organization can use it. `private`: its creator and the people it is shared with. Null for a shortcut published before versioning."},"shared_users":{"type":"array","items":{"type":"string"},"description":"Get Shortcut only. Emails it is shared with (use access). Populated for the shortcut's creator; otherwise `[]`."},"input_schema":{"type":"array","items":{"$ref":"#/components/schemas/InputField"},"description":"Ordered list of input fields — derived from the workflow's input action (id1)"},"output_schema":{"type":"array","items":{"$ref":"#/components/schemas/OutputConfig"},"description":"Ordered list of output fields — derived from the workflow's OUTPUT_CUSTOM_ENRICHMENT action"},"is_master":{"type":"boolean","description":"True if this is the original shortcut (not a shared copy)","example":true},"is_published":{"type":"boolean","description":"Whether the shortcut is published and available for runs","example":true},"is_archived":{"type":"boolean","description":"Whether the shortcut has been archived","example":false},"callable":{"type":"boolean","description":"Whether a workflow can call this shortcut: set its `id` as `shortcut` on a `call_shortcut` action. False until the shortcut has a published version; republish it in the Floqer app.","example":true},"extensions":{"type":"object","additionalProperties":false,"properties":{"linkedin":{"type":"string","nullable":true,"description":"The input the LinkedIn extension fills from the profile page, by its `reference`.","example":"linkedin_url"},"sales_navigator":{"type":"string","nullable":true,"description":"The input the Sales Navigator extension fills from the page, by its `reference`.","example":"linkedin_url"}},"description":"Browser extensions that can trigger this shortcut, each with the input (by `reference`) it fills from the page. `{}` when none can. A `null` input means that extension is on but its input is no longer on the shortcut, so it fills nothing: set a new input or turn it off.","example":{"linkedin":"linkedin_url"}},"cover_image":{"type":"string","nullable":true,"description":"URL of the shortcut's cover image"},"icon_emoji":{"type":"string","nullable":true,"description":"Emoji used as the shortcut icon","example":"🏢"},"created_at":{"type":"string","format":"date-time","description":"When the shortcut was created"},"updated_at":{"type":"string","format":"date-time","description":"When the shortcut was last updated"}}},"ShortcutRun":{"type":"object","description":"Record of a single shortcut execution. Tracks input data, output results, and execution status from creation to completion.","properties":{"id":{"type":"string","description":"Unique run identifier (UUID)","example":"f1a2b3c4-d5e6-7f8a-9b0c-1d2e3f4a5b6c"},"app_id":{"type":"string","description":"ID of the shortcut this run belongs to"},"workflow_id":{"type":"string","description":"ID of the underlying workflow that was executed"},"data_id":{"type":"string","description":"ID of the data row created in the workflow for this run"},"catalog_release_version":{"type":"integer","minimum":1,"description":"Published source shortcut version used by this global run. Absent for ordinary shortcuts."},"credits_used":{"type":"number","nullable":true,"description":"Actual net credits charged, after refunds. Null when unavailable."},"origin":{"type":"object","properties":{"type":{"type":"string","enum":["listing","workflow","api","manual","ackdb"]},"workflow_id":{"type":"string","nullable":true},"workflow_name":{"type":"string","nullable":true},"data_id":{"type":"string","nullable":true},"action_id":{"type":"string","nullable":true}}},"steps":{"type":"array","nullable":true,"description":"Read-only step results from the executed version; requested with include_steps=true.","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"action_name":{"type":"string"},"status":{"type":"string"},"error":{"type":"string","nullable":true},"credits_used":{"type":"number","nullable":true},"outputs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"value":{},"type":{"type":"string","enum":["sectionList"]},"columns":{"type":"array","description":"List columns from the executed version.","items":{"type":"object","properties":{"responseId":{"type":"string"},"responseStructureId":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"}}}}}}}}}},"steps_error":{"type":"string","nullable":true},"input_schema":{"type":"array","items":{"$ref":"#/components/schemas/InputField"},"description":"Frozen input contract for this global run."},"output_schema":{"type":"array","items":{"$ref":"#/components/schemas/OutputConfig"},"description":"Frozen output contract for this global run."},"user_email":{"type":"string","description":"Email of the user who triggered the run"},"org_id":{"type":"string","description":"Organization ID"},"input_data":{"type":"object","additionalProperties":true,"description":"Input values provided when the run was started, keyed by each field's `reference` from the shortcut's `input_schema`"},"output_data":{"type":"object","additionalProperties":true,"nullable":true,"description":"Output values produced by the workflow, keyed by each output field's `name` from the shortcut's `output_schema`. Null until the run completes."},"status":{"type":"string","enum":["pending","inProgress","completed","failed","payloadFilled","readyToBeReviewed","checkingNextSource","conditionNotMet","error","outOfCredits","duplicate"],"description":"Current execution status. Terminal states: completed, failed, error, outOfCredits. In-progress states: pending, inProgress, payloadFilled, readyToBeReviewed, checkingNextSource.","example":"pending"},"error_message":{"type":"string","nullable":true,"description":"Error details if the run failed. Null on success."},"created_at":{"type":"string","format":"date-time","description":"When the run was created"},"completed_at":{"type":"string","nullable":true,"format":"date-time","description":"When the run finished (completed or failed). Null while in progress."}}},"CreateShortcutBody":{"type":"object","description":"Body for Create Shortcut. `workflow_id`, `name` and `outputs` are required; the input schema is read from the workflow's input columns.","properties":{"workflow_id":{"type":"string","description":"UUID of the workflow to package. It must belong to your organization and have at least one named input column.","example":"531952c2-3d07-4be2-8c4b-733acba3187b"},"name":{"type":"string","minLength":1,"description":"Shortcut display name.","example":"Company Enrichment"},"description":{"type":"string","description":"What the shortcut does, shown in the Gallery and returned by Get Shortcut.","example":"Enriches company data from a LinkedIn URL"},"icon_emoji":{"type":"string","description":"Emoji used as the shortcut icon. Defaults to 🚀.","example":"🏢"},"outputs":{"type":"array","minItems":1,"description":"The fields a run returns, in order. Each item names one output field and points it at a value: a `{{input.<field>}}` or `{{<action_instance_id>.<field>}}` reference, a literal, or a mix. Written onto the workflow's `output_shortcut` action (added to the end of the chain when there is none) and returned as `output_schema`. An output keeps its identity across updates as long as its `name` is unchanged. A reference nothing produces is kept and reported in `warnings`.","items":{"type":"object","properties":{"name":{"type":"string","minLength":1,"description":"Output field name — the key its value appears under in a run's `output_data`.","example":"Company Website"},"value":{"type":"string","description":"Where the value comes from.","example":"{{enrich_company_linkedin_profile_1.company_website}}"}},"required":["name","value"],"additionalProperties":false},"example":[{"name":"Company Website","value":"{{enrich_company_linkedin_profile_1.company_website}}"},{"name":"Employee Count","value":"{{enrich_company_linkedin_profile_1.employee_count}}"}]},"extensions":{"type":"object","additionalProperties":false,"properties":{"linkedin":{"type":"string","nullable":true,"description":"The input the LinkedIn extension fills from the profile page, by its `reference`.","example":"linkedin_url"},"sales_navigator":{"type":"string","nullable":true,"description":"The input the Sales Navigator extension fills from the page, by its `reference`.","example":"linkedin_url"}},"description":"Browser extensions that can trigger the shortcut. Each key turns one on and names the input (by `reference`, the snake_cased input column name) the extension fills from the page. Omit it to leave the shortcut off every extension.","example":{"linkedin":"linkedin_url","sales_navigator":"linkedin_url"}}},"required":["workflow_id","name","outputs"],"additionalProperties":false},"UpdateShortcutBody":{"type":"object","minProperties":1,"additionalProperties":false,"description":"Body for Update Shortcut. Send only what changes. `outputs` replaces the previous outputs and publishes a new version; `name`, `description`, `icon_emoji` and `visibility` apply immediately.","properties":{"name":{"type":"string","minLength":1,"description":"New display name.","example":"Company Enrichment v2"},"description":{"type":"string","description":"New description."},"icon_emoji":{"type":"string","description":"New icon emoji.","example":"🏢"},"outputs":{"type":"array","minItems":1,"description":"The fields a run returns, in order. Each item names one output field and points it at a value: a `{{input.<field>}}` or `{{<action_instance_id>.<field>}}` reference, a literal, or a mix. Written onto the workflow's `output_shortcut` action (added to the end of the chain when there is none) and returned as `output_schema`. An output keeps its identity across updates as long as its `name` is unchanged. A reference nothing produces is kept and reported in `warnings`.","items":{"type":"object","properties":{"name":{"type":"string","minLength":1,"description":"Output field name — the key its value appears under in a run's `output_data`.","example":"Company Website"},"value":{"type":"string","description":"Where the value comes from.","example":"{{enrich_company_linkedin_profile_1.company_website}}"}},"required":["name","value"],"additionalProperties":false},"example":[{"name":"Company Website","value":"{{enrich_company_linkedin_profile_1.company_website}}"},{"name":"Employee Count","value":"{{enrich_company_linkedin_profile_1.employee_count}}"}]},"extensions":{"type":"object","additionalProperties":false,"properties":{"linkedin":{"type":"string","nullable":true,"description":"The input the LinkedIn extension fills from the profile page, by its `reference`.","example":"linkedin_url"},"sales_navigator":{"type":"string","nullable":true,"description":"The input the Sales Navigator extension fills from the page, by its `reference`.","example":"linkedin_url"}},"description":"Change which browser extensions can trigger the shortcut. A surface with an input `reference` turns it on or moves it to that input, `null` turns it off, and surfaces you leave out keep their setting. `{}` turns every extension off. Omit the field to leave the setting unchanged. Applies immediately, with no new version; the last update wins. When an `outputs` republish removes an input an extension fills, that extension is turned off and reported in `warnings`.","example":{"sales_navigator":null}},"visibility":{"type":"string","enum":["org","private"],"description":"Who can use the shortcut. `org`: everyone in your organization. `private`: you and the people it is shared with (Share Shortcut)."},"force":{"type":"boolean","description":"Proceed even when the change breaks another workflow's Call Shortcut cell: new `outputs` that remove a field it reads (409 `SHORTCUT_CONTRACT_BREAK` otherwise), or `visibility: \"private\"` while it still calls the shortcut (409 `SHORTCUT_HAS_CALLERS` otherwise). Those call sites break. Defaults to false."}}},"RunShortcutCsvBody":{"type":"object","description":"Body for Run Shortcut from CSV. The file must already be uploaded to Floqer; pass its object key.","properties":{"file_ref":{"type":"string","description":"Object key of the already-uploaded file, exactly as Floqer issued it. Not a URL — a URL is rejected. The file must have been uploaded by someone in your organization.","example":"leads.csv_jane@acme.com_9f3c1a2b-4d5e-6f70-8a9b-0c1d2e3f4a5b.csv"},"mapping":{"type":"object","additionalProperties":{"type":"string"},"description":"Column header in the file -> input `reference` on the shortcut. References come from Get Shortcut's `input_schema[].reference` — the same keys Run Shortcut takes in `input_data`. Headers are read from row 1; naming a header the file does not have fails the import and reports which one. Columns omitted here are not sent, and inputs with no column fall back to the shortcut's `defaultValue`. Two columns may not map to the same input.","example":{"Email":"email","Company Name":"company_name","LinkedIn URL":"linkedin_url"}},"sheet_name":{"type":"string","description":"XLSX only — which worksheet to run. Defaults to the first sheet in the workbook. Naming a sheet the workbook does not have fails the import."}},"required":["file_ref","mapping"]},"RunShortcutCsvData":{"type":"object","description":"Acknowledgement that the import was accepted and queued. Runs appear on List Shortcut Runs filtered by `batch_id` as the file streams.","properties":{"status":{"type":"string","enum":["processing"],"description":"Always `processing` — the import runs in the background."},"batch_id":{"type":"string","format":"uuid","description":"The batch every run from this file shares. Poll `GET /api/v1/shortcuts/{id}/runs?batch_id=...`; an empty list means the import has not reached its first row yet, and a failure before the first row appears there as a single failed run carrying the reason."},"columns_mapped":{"type":"integer","description":"How many of the file's columns will be sent — the number of entries in `mapping`."}},"required":["status","batch_id","columns_mapped"]},"ExportShortcutRunsBody":{"type":"object","description":"Body for Export Shortcut Runs. Names the batch whose runs become the CSV.","properties":{"batch_id":{"type":"string","description":"The batch to export, as returned by Run Shortcut Batch or Import CSV for Shortcuts. The same id List Shortcut Runs filters by. You can only export a batch you ran."},"file_name":{"type":"string","description":"Base name for the downloaded file, without extension. Defaults to `shortcut-runs`. Non-alphanumeric characters are replaced and the name is capped at 100 characters."}},"required":["batch_id"]},"ShortcutExportJob":{"type":"object","description":"An export job. Poll Get Shortcut Runs Export until `status` is `completed` or `failed`.","properties":{"export_id":{"type":"string","description":"ID of this export. Pass it to Get Shortcut Runs Export to poll. Valid for 24 hours."},"status":{"type":"string","enum":["running","completed","failed"],"description":"`running` — queued or building. `completed` — `file_url` is ready. `failed` — see `error`. There is no separate queued state; a job is `running` from the moment it is accepted."},"progress":{"type":"integer","description":"Percent complete, 0-100. Reaches 100 only when `status` is `completed`."},"row_count":{"type":"integer","description":"Runs the export will contain, counted against the batch when the job was accepted — never an echo of the request."},"file_url":{"type":"string","description":"Time-limited download link, present only when `status` is `completed`. Minted fresh on every poll and valid for 15 minutes — it is a signed URL, so treat it as a secret and do not log or forward it."},"expires_at":{"type":"string","format":"date-time","description":"When `file_url` stops working (UTC, ISO 8601). Present only alongside `file_url`."},"error":{"type":"string","description":"Why the export failed. Present only when `status` is `failed`."},"created":{"type":"boolean","description":"Export Shortcut Runs only. `true` when this call started the job; `false` when an export was already building for you on this shortcut and you were handed that one instead."},"started_at":{"type":"string","format":"date-time","description":"When the job was accepted (UTC, ISO 8601)."},"completed_at":{"type":["null","string"],"format":"date-time","description":"When the job reached a terminal state (UTC, ISO 8601). `null` while still running."}},"required":["export_id","status","progress","row_count","started_at","completed_at"]},"FloqerShortcut":{"type":"object","properties":{"id":{"type":"string","pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"},"name":{"type":"string"},"description":{"type":"string"},"is_listed":{"type":"boolean"},"latest_release":{"type":"integer","description":"Current published source shortcut version."},"created_at":{"type":"string"},"updated_at":{"type":"string"}}},"CampaignLeadField":{"type":"object","description":"A field in the campaign's lead schema. Use `name` as the key when adding leads; `email` and `name` are fixed fields present on every campaign.","properties":{"name":{"type":"string","description":"Field name — the key to use in a lead object","example":"company"},"type":{"type":"string","description":"Declared field type","example":"string"},"required":{"type":"boolean","description":"Whether the field must be provided"}},"additionalProperties":true},"Campaign":{"type":"object","description":"A campaign runs leads through a published sequence of email and task steps. It sends only when it has a published sequence and `state` is `active`.","properties":{"id":{"type":"string","description":"Campaign ID (UUID)"},"name":{"type":"string","description":"Campaign name","example":"Q3 Outbound"},"description":{"type":"string","description":"Campaign description"},"state":{"type":"string","enum":["active","paused"],"description":"`active` sends on schedule; `paused` halts sending. New campaigns start paused."},"has_published_sequence":{"type":"boolean","description":"Whether a sequence has been published. A campaign with no published sequence never enrols leads."},"is_archived":{"type":"boolean","description":"Whether the campaign has been archived"},"settings":{"type":"object","additionalProperties":true,"description":"Sending settings: `sending_window_start` / `sending_window_end` (HH:MM), `timezone`, `remove_on_reply`, `create_task_on_reply`, `bounce_rate_limit`."},"lead_schema":{"type":"array","items":{"$ref":"#/components/schemas/CampaignLeadField"},"description":"The campaign's lead fields. Returned by Get Campaign only."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"CampaignLead":{"type":"object","description":"A lead in a campaign. `email` is the identity and dedup key — re-adding the same address updates it.","properties":{"id":{"type":"string","description":"Lead ID, derived deterministically from the email address"},"email":{"type":"string","description":"Lead email address"},"name":{"type":"string","description":"Lead name"},"status":{"type":"string","enum":["added","in_sequence","completed","removed","replied","responded","unsubscribed","suppressed","bounced"],"description":"`added` (awaiting enrolment), `in_sequence` (running), then a terminal state: `completed`, `replied`, `responded`, `bounced`, `unsubscribed`, `suppressed`, `removed`."},"email_provider":{"type":"string","nullable":true,"description":"Mail provider derived from the address's MX records, e.g. `google`, `microsoft`, `unknown`"},"fields":{"type":"object","additionalProperties":true,"description":"Custom field values, keyed by field name from the campaign's `lead_schema`"},"current_node_id":{"type":"string","nullable":true,"description":"Sequence node the lead is currently at"},"next_run_at":{"type":"string","nullable":true,"format":"date-time","description":"When the lead's next step is due"},"replied_at":{"type":"string","nullable":true,"format":"date-time"},"created_at":{"type":"string","format":"date-time"}}},"CampaignSequenceNode":{"type":"object","description":"A step in the sequence. Nodes form a single linear chain from the `start` node via `next_node_id`; `wait` nodes between steps are managed automatically.","properties":{"node_id":{"type":"string","description":"Node ID — stable for the life of the node","example":"send-m2k9x1p-0"},"type":{"type":"string","enum":["start","wait","send","manual_task","phone_call_task","linkedin_message_task","note"],"description":"Persisted node type. A `manual_email` added via the catalog persists as `send` with `config.send_mode = \"manual\"`."},"display_name":{"type":"string","nullable":true,"description":"Custom step name; falls back to the type label"},"config":{"type":"object","additionalProperties":true,"description":"Step configuration, stored and returned verbatim in the sending engine's own vocabulary — so key casing is mixed (`sender_mode` next to `trackOpens`) and it is passed through rather than renamed, because these are the exact keys the sender reads.\n\n**send**: `subject`, `body` (required), plus `send_mode` (`manual`), `thread_mode` (`reply`|`new`), `cc`, `bcc`, `trackOpens`, `sender_mode` (`pool`|`mailbox`|`member`), `sender_mailbox_id`, `sender_member_email`, `replyTaskPriority`. **wait**: `delay`, `unit` (`minutes`|`hours`|`days`). **task steps**: `priority`, `note`, `assignee_mode` (`mailbox_owner`|`member`), `assignee_email`. **start**: `leadFields` — the campaign's lead schema, in the same shape as `lead_schema` on Get Campaign."},"next_node_id":{"type":"string","nullable":true,"description":"The next step in the chain; null on the final step"}}},"CampaignNodeCatalogEntry":{"type":"object","description":"A step type that can be added to a sequence. Pass `id` as `type` in Add Node.","properties":{"id":{"type":"string","description":"Catalog id — pass this as `type` when adding a node","example":"manual_email"},"node_type":{"type":"string","description":"The type this persists as","example":"send"},"label":{"type":"string","example":"Manual Email"},"description":{"type":"string"},"category":{"type":"string","enum":["automatic","manual"]},"default_config":{"type":"object","additionalProperties":true,"description":"Config applied when the node is created"},"fields":{"type":"array","description":"The config fields surfaced in the builder. Fields marked required are enforced when adding or updating a node.","items":{"type":"object","additionalProperties":true,"properties":{"key":{"type":"string"},"label":{"type":"string"},"type":{"type":"string"},"required":{"type":"boolean"},"options":{"type":"array","items":{"type":"object","additionalProperties":true}}}}}}},"CampaignMailbox":{"type":"object","description":"A connected mailbox in the organization, flagged with whether this campaign sends from it.","properties":{"id":{"type":"string","description":"Mailbox ID — pass in `mailbox_ids` to select it"},"email":{"type":"string","description":"The mailbox's address"},"provider":{"type":"string","description":"Mail provider","example":"google"},"owner_email":{"type":"string","description":"Org member who connected the mailbox"},"daily_cap":{"type":"integer","description":"Maximum sends per day from this mailbox"},"status":{"type":"string","description":"Connection status; only live mailboxes send"},"selected":{"type":"boolean","description":"Whether this campaign currently sends from it"}}},"CampaignMetrics":{"type":"object","description":"Aggregate campaign performance. Counts are derived on read from the per-step records.","properties":{"leads":{"type":"integer","description":"Total leads in the campaign"},"leads_sent":{"type":"integer","description":"Leads that have received at least one email"},"emails_sent":{"type":"integer"},"opened":{"type":"integer"},"replied":{"type":"integer"},"positive":{"type":"integer","description":"Replies classified as positive. **Not populated yet** — no reply classifier exists, so this is always 0."},"bounced":{"type":"integer"},"open_rate":{"type":"number"},"reply_rate":{"type":"number"},"positive_reply_rate":{"type":"number","description":"Always 0 until reply classification ships — see `positive`. Do not build on it yet."},"bounce_rate":{"type":"number"},"by_status":{"type":"object","additionalProperties":true,"description":"Lead counts keyed by lead status"},"trend":{"type":"object","additionalProperties":true,"description":"Sends bucketed over time: `{ granularity, tz, buckets: [{ bucket, sent }] }`"}}},"AckdbSyncCounters":{"type":"object","properties":{"delivered":{"type":"integer","description":"Rows delivered to the workflow, all time."},"exited_since_delivery":{"type":"integer","description":"Delivered rows whose member has since exited the segment."},"suppressed_reentries":{"type":"integer","description":"Re-entries suppressed by the connection's re-entry policy."},"held":{"type":"integer","description":"Rows held by a pause or breaker."},"pending":{"type":"integer","description":"Rows awaiting delivery."},"added":{"type":"integer","description":"Added events delivered by an event-ledger sync."},"removed":{"type":"integer","description":"Removed events delivered by an event-ledger sync."}},"required":["delivered","exited_since_delivery","suppressed_reentries","held","pending","added","removed"]},"AckdbSyncConnection":{"type":"object","properties":{"connection_id":{"type":"string","description":"Durable sync-connection id."},"workflow_id":{"type":"string","format":"uuid","description":"Destination Floq workflow."},"status":{"type":"string","enum":["active","paused"]},"delivery_mode":{"type":"string","enum":["current_state","event_ledger_v1"],"description":"Whether the workflow stores current membership or one immutable row per segment event."},"reentry_mode":{"type":"string","enum":["suppress","repush"],"description":"Whether a member re-entering the segment is delivered again."},"push_existing":{"type":"boolean","description":"Whether members present at connection creation were seeded for delivery."},"run":{"type":"string","enum":["all","none"],"description":"Whether delivered event rows execute the workflow or only insert rows."},"field_mapping":{"type":"object","additionalProperties":true,"description":"Workflow input reference to ackDB segment field mapping."},"mapping_warnings":{"type":"array","items":{"type":"string"},"description":"Stored mappings that could not be represented as current public workflow input references."},"created_at":{"type":["null","string"],"format":"date-time"},"counters":{"$ref":"#/components/schemas/AckdbSyncCounters"}},"required":["connection_id","workflow_id","status","delivery_mode","reentry_mode","push_existing","run","field_mapping","mapping_warnings","created_at","counters"]},"AckdbSync":{"type":"object","properties":{"synced":{"type":"boolean"},"sync_id":{"type":["null","string"],"description":"Floqer watcher id for the durable sync."},"name":{"type":["null","string"]},"segment_id":{"type":"string"},"entity_kind":{"type":["null","string"],"enum":["company","contact","person",null]},"state":{"type":["null","string"],"enum":["seeding","active","paused","pending_confirmation",null]},"segment_size":{"type":["null","integer"]},"segment_size_is_lower_bound":{"type":"boolean"},"last_import_error":{"type":["null","object"],"properties":{"code":{"type":["null","string"]},"message":{"type":"string"}},"required":["code","message"]},"pending_confirmation":{"type":["null","object"],"properties":{"held_count":{"type":"integer"},"reason":{"type":["null","string"],"enum":["filter_changed","flood",null]}},"required":["held_count","reason"]},"created_by":{"type":["null","string"]},"created_at":{"type":["null","string"],"format":"date-time"},"connection_count":{"type":"integer"},"connections":{"type":"array","items":{"$ref":"#/components/schemas/AckdbSyncConnection"}}},"required":["synced","sync_id","name","segment_id","entity_kind","state","segment_size","segment_size_is_lower_bound","last_import_error","pending_confirmation","created_by","created_at","connection_count","connections"]},"AckdbCreateSyncBody":{"type":"object","additionalProperties":false,"properties":{"workflow_id":{"type":"string","format":"uuid"},"field_mapping":{"type":"object","minProperties":1,"additionalProperties":{"type":"string","minLength":1},"description":"Map workflow input references to ackDB segment fields, e.g. `{ \"input.linkedin_url\": \"linkedin_url\" }`."},"push_existing":{"type":"boolean","default":false,"description":"Deliver members already in the segment. Defaults to false."},"run":{"type":"string","enum":["all","none"],"default":"none","description":"Run the workflow on delivered rows, or insert them without running. Defaults to none."},"reentry_mode":{"type":"string","enum":["suppress","repush"],"default":"suppress","description":"Deprecated for new event-ledger syncs, which always append re-entry as an added event."}},"required":["workflow_id","field_mapping"]},"AckdbPatchSyncConnectionBody":{"type":"object","minProperties":1,"additionalProperties":false,"properties":{"status":{"type":"string","enum":["paused"],"description":"Pause this destination. Resume uses the dedicated endpoint because it needs a backlog choice."},"reentry_mode":{"type":"string","enum":["suppress","repush"],"description":"Legacy current-state syncs only; event-ledger re-entry behavior is fixed."},"field_mapping":{"type":"object","minProperties":1,"additionalProperties":{"type":"string","minLength":1},"description":"Complete replacement mapping. Already-delivered rows are not re-pushed."}}},"AckdbResumeSyncConnectionBody":{"type":"object","additionalProperties":false,"properties":{"backlog":{"type":"string","enum":["deliver","skip"]}},"required":["backlog"]},"AckdbConfirmSyncBody":{"type":"object","additionalProperties":false,"properties":{"action":{"type":"string","enum":["push","skip"]}},"required":["action"]},"AckdbSyncErrorResponse":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"required":["status","error","message"]},"AckdbObserveFilter":{"type":"object","description":"Recursive Observe filter DSL. A node is either a boolean `group` (with `op` and a `children` array of further nodes) or a leaf condition (`trait`, `attribute`, `event`, or `event_field`). The root node is always a group. Empty groups compile to TRUE (match all).","oneOf":[{"$ref":"#/components/schemas/AckdbObserveFilterGroup"},{"$ref":"#/components/schemas/AckdbObserveTraitCondition"},{"$ref":"#/components/schemas/AckdbObserveAttributeCondition"},{"$ref":"#/components/schemas/AckdbObserveEventCondition"},{"$ref":"#/components/schemas/AckdbObserveEventFieldCondition"}]},"AckdbObserveFilterGroup":{"type":"object","description":"A boolean group combining child nodes with AND/OR.","properties":{"type":{"type":"string","enum":["group"]},"op":{"type":"string","enum":["and","or"],"description":"How children are combined."},"children":{"type":"array","items":{"$ref":"#/components/schemas/AckdbObserveFilter"}}},"required":["type","op","children"]},"AckdbObserveTraitCondition":{"type":"object","description":"A condition on a registered entity trait. `traitKey` is whitelisted against the field catalog; the operator must be legal for the trait's valueType.","properties":{"type":{"type":"string","enum":["trait"]},"traitKey":{"type":"string","example":"hubspot_lifecycle_stage"},"operator":{"type":"string","enum":["eq","neq","gt","gte","lt","lte","contains","not_contains","in","is_null","is_not_null","in_last_days","more_than_days_ago","in_next_days"]},"value":{"description":"Scalar or array depending on the operator. Omitted for is_null/is_not_null. For in_last_days, more_than_days_ago and in_next_days, a positive integer day count on a date field, at most 36500 days.","oneOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"array","items":{"oneOf":[{"type":"string"},{"type":"number"}]}}]}},"required":["type","traitKey","operator"]},"AckdbObserveAttributeCondition":{"type":"object","description":"A condition on a fixed entity attribute (name, domain, status, timestamps, event counts, sources, last activity, or source presence).","properties":{"type":{"type":"string","enum":["attribute"]},"field":{"type":"string","enum":["name","created_at","updated_at","domain","status","event_count","last_active","sources","source_present"]},"operator":{"type":"string","enum":["eq","neq","contains","not_contains","in","gt","gte","lt","lte","within_days","is_null","is_not_null","in_last_days","more_than_days_ago","in_next_days"]},"value":{"description":"Scalar or array depending on the operator. Omitted for is_null/is_not_null. For in_last_days, more_than_days_ago and in_next_days, a positive integer day count on a date field, at most 36500 days.","oneOf":[{"type":"string"},{"type":"number"},{"type":"array","items":{"oneOf":[{"type":"string"},{"type":"number"}]}}]}},"required":["type","field","operator"]},"AckdbObserveEventCondition":{"type":"object","description":"A condition on the entity's event timeline. `eventType` is source-prefixed and whitelisted against the catalog. `withinDays` bounds the lookback; `count` is used by count_gte/count_lte.","properties":{"type":{"type":"string","enum":["event"]},"eventType":{"type":"string","example":"stripe.invoice.paid"},"source":{"type":"string","example":"stripe"},"predicate":{"type":"string","enum":["exists","not_exists","count_gte","count_lte"]},"count":{"type":"integer","minimum":0,"example":3},"withinDays":{"type":"integer","minimum":1,"example":30}},"required":["type","eventType","predicate"]},"AckdbObserveEventFieldCondition":{"type":"object","description":"A condition on a promoted event-metadata field's reduced scalar value (#212). `columnKey` is the synthetic field-catalog key (`emf__<source>__<field>` for the latest value, or `…__sum` for a numeric running total) and is whitelisted against the catalog; the operator must be legal for the field's valueType (same legality as a trait of that type).","properties":{"type":{"type":"string","enum":["event_field"]},"columnKey":{"type":"string","example":"emf__stripe__amount"},"operator":{"type":"string","enum":["eq","neq","gt","gte","lt","lte","contains","not_contains","in","is_null","is_not_null","in_last_days","more_than_days_ago","in_next_days"]},"value":{"description":"Scalar or array depending on the operator. Omitted for is_null/is_not_null. For in_last_days, more_than_days_ago and in_next_days, a positive integer day count on a date field, at most 36500 days.","oneOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"array","items":{"oneOf":[{"type":"string"},{"type":"number"}]}}]}},"required":["type","columnKey","operator"]},"AckdbSegmentSort":{"type":"object","description":"One sort directive: a field key and a direction.","properties":{"field":{"type":"string","example":"event_count"},"dir":{"type":"string","enum":["asc","desc"]}},"required":["field","dir"]},"AckdbSegmentColumn":{"type":"object","description":"One projected column: a field key, optionally frozen to the left edge, optionally a persisted pixel width. Width is per-segment layout (presentation only) - the compiler ignores it.","properties":{"field":{"type":"string","example":"name"},"frozen":{"type":"boolean","example":true},"width":{"type":"number","example":200,"description":"Persisted column width in px (per-segment; presentation only)."}},"required":["field"]},"AckdbSegment":{"type":"object","description":"A saved Observe Segment - config only (no data snapshot). Owned + per-user shared (#284); soft-deleted via deleted_at.","properties":{"id":{"type":"string","description":"ULID.","example":"01J9Z8Q3K2X7M4N5P6R7S8T9V0"},"name":{"type":"string","example":"Active paying companies"},"description":{"type":["string","null"],"example":"Companies with a paid Stripe invoice"},"entityKind":{"type":"string","enum":["company","contact","person"],"example":"company","description":"Home (authoring) grain - the vocabulary the filter/sort/columns are written in. Every segment is also projectable to the other grains at read time via GET /observe/segments/{id}/results?grain=…, so this is where the filter was authored, not the only grain it can render."},"filter":{"$ref":"#/components/schemas/AckdbObserveFilterGroup"},"sort":{"type":"array","items":{"$ref":"#/components/schemas/AckdbSegmentSort"}},"columns":{"type":"array","items":{"$ref":"#/components/schemas/AckdbSegmentColumn"}},"isDefault":{"type":"boolean","description":"True when this Segment is the default view for its entity_kind (one per kind).","example":false},"baseSegmentId":{"type":["string","null"],"description":"DERIVED segment: the base segment this one is built on. When set, entityKind is the OPPOSITE of the base's grain, `filter` is the render-grain filter on top, and GET /results renders the base's filter pivoted into this grain AND this filter - composed at read time, no stored id set. null for a normal (root) segment.","example":null},"ownerEmail":{"type":["string","null"],"description":"The segment's OWNER (#284) - the creator's JWT email. null = legacy / service-created = grandfathered org-visible + editable. Stamped server-side on create (never accepted from the client) and immutable thereafter; sharing with teammates is per-member via /shares.","example":"rep@acme.com"},"access":{"type":"string","enum":["owner","edit","view"],"description":"The CALLER's effective role on this segment (#284). Present on single reads; drives client action gating (owner/edit may save; owner only may delete / manage shares / set default).","example":"owner"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","name","entityKind","filter","sort","columns","isDefault","baseSegmentId","ownerEmail","createdAt","updatedAt"]},"AckdbSegmentSummary":{"type":"object","description":"Lightweight Segment list-item (no filter/sort/columns).","properties":{"id":{"type":"string","example":"01J9Z8Q3K2X7M4N5P6R7S8T9V0"},"name":{"type":"string","example":"Active paying companies"},"description":{"type":["string","null"],"example":"Companies with a paid Stripe invoice"},"entityKind":{"type":"string","enum":["company","contact","person"],"example":"company"},"isDefault":{"type":"boolean","description":"True when this Segment is the default view for its entity_kind.","example":false},"baseSegmentId":{"type":["string","null"],"description":"Base segment id when this is a derived segment (see Segment.baseSegmentId); else null.","example":null},"ownerEmail":{"type":["string","null"],"description":"The segment's owner (JWT email), or null for a legacy/service-created row (#284).","example":"rep@acme.com"},"access":{"type":"string","enum":["owner","edit","view"],"description":"The caller's effective role on this row (#284) — drives client action gating.","example":"owner"},"folderId":{"type":["string","null"],"description":"The folder THIS CALLER has filed this segment in, or null for 'Unfiled' (#470). Folders are personal: the same segment is filed differently (or not at all) for every other user, and a service/static-key call always reads null.","example":"01JA0B1C2D3E4F5G6H7J8K9M0N"},"folderPosition":{"type":["integer","null"],"description":"Manual order within `folderId`, ascending. Null when unfiled.","example":0},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","name","entityKind","isDefault","baseSegmentId","ownerEmail","access","folderId","folderPosition","createdAt","updatedAt"]},"AckdbSegmentFolder":{"type":"object","description":"A folder in ONE user's segment rail (#470). Folders are a personal filing layer: they are created, read, and written only by their owner, they are flat (a folder never contains another folder), and they span grains — one folder can hold company, contact and person segments, which is what keeps a derived segment filed next to the base it was pivoted from.","properties":{"id":{"type":"string","example":"01JA0B1C2D3E4F5G6H7J8K9M0N"},"name":{"type":"string","example":"Q3 Enterprise Push"},"ownerEmail":{"type":"string","description":"The owner's lowercased email. Always the caller — folders are personal.","example":"rep@acme.com"},"position":{"type":"integer","description":"Manual rail order within the owner's set, ascending.","example":0},"segmentCount":{"type":"integer","description":"Segments in this folder the caller can currently SEE, narrowed to the requested `entityKind` (all grains when none is given). Not a raw row count: a segment that was unshared from the caller keeps its placement but is not counted, since the rail cannot render it. Present on the list response only.","example":4},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","name","ownerEmail","position","createdAt","updatedAt"]},"AckdbSegmentFolderItem":{"type":"object","description":"One segment's placement inside a folder.","properties":{"segmentId":{"type":"string","example":"01J9Z8Q3K2X7M4N5P6R7S8T9V0"},"position":{"type":"integer","description":"Manual order within the folder, ascending.","example":0}},"required":["segmentId","position"]},"AckdbSegmentShare":{"type":"object","description":"One per-member grant on a segment (#284): a member from the GTM roster (memberId) + the role they hold. name/email are joined from the member row for display.","properties":{"memberId":{"type":"string","example":"01J9Z8Q3K2X7M4N5P6R7S8T9V0"},"role":{"type":"string","enum":["view","edit"],"example":"edit"},"name":{"type":["string","null"],"example":"Jordan Rep"},"email":{"type":["string","null"],"example":"jordan@acme.com"},"createdAt":{"type":"string","format":"date-time"}},"required":["memberId","role","name","email"]},"AckdbDefaultSegmentResponse":{"type":"object","description":"The default view for a kind. Either an explicitly-marked Segment (source 'explicit', id non-null) or the server-computed catalog fallback (source 'computed', id null). Clients seed a fresh table's columns+sort from this without hard-coding the column policy.","properties":{"id":{"type":["string","null"],"example":"01J9Z8Q3K2X7M4N5P6R7S8T9V0"},"name":{"type":["string","null"],"example":"Active paying companies"},"description":{"type":["string","null"]},"entityKind":{"type":"string","enum":["company","contact","person"],"example":"company"},"filter":{"$ref":"#/components/schemas/AckdbObserveFilterGroup"},"sort":{"type":"array","items":{"$ref":"#/components/schemas/AckdbSegmentSort"}},"columns":{"type":"array","items":{"$ref":"#/components/schemas/AckdbSegmentColumn"}},"isDefault":{"type":"boolean","example":true},"source":{"type":"string","enum":["explicit","computed"],"example":"computed"},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"}},"required":["id","entityKind","filter","sort","columns","isDefault","source","createdAt","updatedAt"]},"AckdbFieldLabelOverride":{"type":"object","description":"A per-deployment override of an Observe field's DISPLAY label (#191). Presentation only - the key, compiler whitelist, filters, sort, and ingest are untouched.","properties":{"scope":{"type":"string","enum":["company","contact","person"],"example":"company"},"fieldKey":{"type":"string","example":"measure_status"},"label":{"type":"string","example":"Customer Status"},"updatedAt":{"type":"string","format":"date-time"}},"required":["scope","fieldKey","label","updatedAt"]},"AckdbObserveFieldEditResponse":{"type":"object","description":"Result of a direct field-value edit (#238). `updated` lists the field keys that were written; `errors` maps each field that was NOT written to a reason - an unknown field, a type/allowedValues violation, or a non-editable field (a computed/derived column, an append-only event/event-field column, or the internal `status` lifecycle enum).","properties":{"data":{"type":"object","properties":{"updated":{"type":"array","items":{"type":"string"},"description":"Field keys successfully written.","example":["measure_status"]},"errors":{"type":"object","additionalProperties":{"type":"string"},"description":"Field key → rejection/validation reason for fields that were not written.","example":{"activity":"Field \"activity\" is computed and cannot be edited directly."}}},"required":["updated","errors"]}},"required":["data"]},"AckdbAggregatableFieldInfo":{"type":"object","description":"A promoted event field the event-aggregate composer (#413) may reference. `numeric` fields are eligible for sum/avg/min/max; every field is usable for count + where conditions.","properties":{"field":{"type":"string","example":"amount_paid_cents"},"label":{"type":"string","example":"Amount paid"},"valueType":{"type":"string","enum":["string","number","boolean","date","string_array"]},"numeric":{"type":"boolean","example":true}},"required":["field","label","valueType","numeric"]},"AckdbFieldDescriptor":{"type":"object","description":"A catalog field available for filtering/sorting/projection in Observe. Coverage is the count of active companies with a value for this field. `group` distinguishes system attributes, traits, and event types.","properties":{"key":{"type":"string","example":"event_count"},"label":{"type":"string","example":"Event Count"},"group":{"type":"string","enum":["system","trait","event"]},"valueType":{"type":"string","enum":["string","number","boolean","date","string_array"]},"source":{"type":"string","example":"system"},"coverage":{"type":"integer","example":1280},"sortable":{"type":"boolean","example":true},"filterable":{"type":"boolean","example":true},"operators":{"type":"array","items":{"type":"string"},"description":"Operators legal for this field (trait/attribute operators or event predicates)."},"allowedValues":{"type":["array","null"],"items":{},"description":"Enum constraint from the trait definition, if any."},"distinctValuesUrl":{"type":"string","description":"Present only when the field supports a distinct-value picker (string/string_array traits, whitelisted system attributes, string/string_array emf__ columns) - the grain-correct URL to fetch values from. Absent means free-text input.","example":"/observe/companies/fields/hubspot_lifecycle_stage/values"}},"required":["key","label","group","valueType","source","coverage","sortable","filterable","operators"]},"AckdbObserveQueryRequest":{"type":"object","description":"Body for POST /observe/companies/query (and the /observe/contacts/query, /observe/persons/query twins). All fields optional - an empty body returns the default page of all active rows. The envelope is validated: an unknown top-level key, or a `group` node carrying an unknown key / a non-array `children`, is a 400 rather than a silently unfiltered query.","properties":{"filter":{"$ref":"#/components/schemas/AckdbObserveFilterGroup"},"sort":{"type":"array","items":{"$ref":"#/components/schemas/AckdbSegmentSort"}},"columns":{"type":"array","items":{"$ref":"#/components/schemas/AckdbSegmentColumn"}},"limit":{"type":"integer","minimum":1,"maximum":1000,"example":50,"description":"Page size, default 50. A limit above 1000 is a 400 - it is never clamped, because a clamped page size that the caller then steps `offset` by skips rows invisibly."},"offset":{"type":"integer","minimum":0,"example":0},"ids":{"type":"array","items":{"type":"string"},"maxItems":500,"description":"Optional explicit id selection (pick specific rows instead of a filter). Each id is the row's grain id - entity_id for companies, contact_id for contacts, person_id for persons. When present, results are restricted to these ids, columns/sort still apply, any filter is ANDed in, and the id set becomes the page (limit/offset ignored). Empty array returns no rows; over 500 ids returns 400."}}},"AckdbObserveQueryResponse":{"type":"object","description":"Response for the Observe company query. `data` rows always carry entityId, name, and domain plus the projected columns. `pagination.totalIsLowerBound` may be set when the count was clamped.","properties":{"data":{"type":"array","items":{"type":"object","description":"A compiled company row - entityId/name/domain plus projected columns.","additionalProperties":true,"properties":{"entityId":{"type":"string"},"name":{"type":["string","null"]},"domain":{"type":["string","null"]}},"required":["entityId"]}},"pagination":{"$ref":"#/components/schemas/AckdbPaginationMeta"}},"required":["data","pagination"]},"AckdbSourceDefinition":{"type":"object","description":"Defines the expected shape of data arriving from a source type. Global defaults ship with ackDB.","properties":{"sourceType":{"type":"string","description":"Source type identifier","example":"slack"},"contentSchema":{"type":"object","description":"Field names → type strings. \"?\" suffix means optional (e.g. \"string?\").","additionalProperties":{"type":"string"},"example":{"channel_id":"string","channel_name":"string","message_text":"string","user":"string","thread_ts":"string?","timestamp":"datetime"}},"identityLinkTypes":{"type":"array","items":{"type":"string"},"description":"External ID types this source can produce.","example":["slack_channel_id","slack_user_id"]},"defaultTraits":{"type":"array","items":{"$ref":"#/components/schemas/AckdbDefaultTraitSpec"},"description":"Traits that auto-populate when an org enables this source."},"validEventTypes":{"type":"array","items":{"type":"string"},"description":"Valid event types for this source, as stored on `entity_events.event_type` (the source's `eventTypePrefix` is prepended to the bare metadata event type). Write API rejects unknown types. This is a denormalized convenience copy - the authoritative, always-current list (with full field schemas and ingest examples) is `GET /schemas/sources/{sourceType}/events`. For Stripe: `stripe_charge.succeeded`, `stripe_charge.refunded`, `stripe_invoice.paid`, `stripe_customer.subscription.created`, etc. For Slack: `slack_message`, `slack_member_joined_channel`, etc.","example":["stripe_customer.subscription.created","stripe_charge.succeeded","stripe_invoice.paid","stripe_charge.refunded"]},"eventTypeField":{"type":["string","null"],"description":"Which content.metadata field determines the event type. Null means the source has a single static event type (e.g. Slack always produces 'message').","example":"event_type"},"eventTypePrefix":{"type":"string","description":"Namespace prefix for event types (e.g. 'stripe_'). When eventTypeField is null, this is used as the full static event type.","example":"stripe_"},"eventMetadataFields":{"type":"array","items":{"type":"string"},"description":"Content.metadata fields to promote into entity_events.metadata for queryable event data. These fields become available for filtering in search_events and aggregation in aggregate_events.","example":["amount_cents","currency","status","customer_id"]},"createdAt":{"type":"string","format":"date-time","description":"When this definition was created."}},"required":["sourceType","contentSchema","identityLinkTypes","defaultTraits","validEventTypes","eventTypeField","eventTypePrefix","eventMetadataFields","createdAt"]},"AckdbDefaultTraitSpec":{"type":"object","description":"A trait that ships with a source definition as a default.","properties":{"traitKey":{"type":"string","description":"Trait identifier, conventionally prefixed with source name.","example":"slack_message_count"},"valueType":{"$ref":"#/components/schemas/AckdbTraitValueType"}},"required":["traitKey","valueType"]},"AckdbEventTypeDefinition":{"type":"object","description":"Complete source event contract, including ownership and optional subject mappings. entity is company-only; person extracts an affiliated actor; person_only resolves one independent human without an affiliation.","properties":{"sourceType":{"type":"string","example":"stripe"},"eventType":{"type":"string","example":"invoice.paid"},"description":{"type":"string","description":"What this event represents (for AI agents and workflow builders)"},"category":{"type":["string","null"],"description":"Cross-source activity category for this event (e.g. `billing`, `communication`, `meeting`). Lets agents filter related events across different sources.","example":"billing"},"contentNature":{"type":["string","null"],"enum":["conversational","structured","activity-signal",null],"description":"How the event's content should be treated downstream (e.g. for embedding). `conversational` content is chunked + embedded; `structured`/`activity-signal` is not.","example":"structured"},"rationale":{"type":"string","description":"Why this event is included and which redundant events it replaces"},"replacesEvents":{"type":"array","items":{"type":"string"},"description":"Source events intentionally excluded because this event covers their signal"},"fields":{"type":"object","description":"Per-field definitions with type, description, computeHint, example","additionalProperties":{"type":"object","properties":{"type":{"type":"string","enum":["string","number","boolean","datetime","string_array"]},"required":{"type":"boolean"},"description":{"type":"string"},"example":{},"computeHint":{"type":"string","description":"How to extract this field from the raw source API"},"allowedValues":{"type":"array","items":{"type":"string"}},"requiredWhen":{"type":"object","description":"Conditional requirement: this otherwise-optional field becomes REQUIRED (ingest 422s without it) when a sibling field's value is one of `valueIn`. Enforced by the ingest validator - not advisory.","properties":{"field":{"type":"string","description":"Sibling field name to inspect."},"valueIn":{"type":"array","items":{"type":"string"},"minItems":1}},"required":["field","valueIn"]}}}},"promotedMetadataFields":{"type":"array","items":{"type":"string"},"description":"Fields promoted to entity_events.metadata for fast queryability"},"traitHints":{"type":"array","description":"Traits that should be updated when this event is ingested","items":{"type":"object","properties":{"traitKey":{"type":"string"},"computeHint":{"type":"string"},"updateStrategy":{"type":"string","enum":["always","first_only","latest"]}}}},"ingestExample":{"type":"object","description":"Complete POST /ingest request body - copy-pasteable template"},"eventScope":{"type":"string","enum":["entity","person","person_only"],"description":"Ownership comes from the event type: entity is company-only, person is a person at a company, person_only is the human alone. Personal requests omit company links (or send empty arrays), forbid non-null entityId/contactId and nonempty contacts/traits/files, and resolve a durable person key or explicit personId. Personal structured/activity events create no affiliations."},"contactMappings":{"type":"array","items":{"$ref":"#/components/schemas/AckdbContactMappingInput"}}},"required":["sourceType","eventType","description","fields"]},"AckdbEventTypeFieldInput":{"type":"object","description":"One field within an event type's content schema. `requiredWhen` makes an otherwise-optional field conditionally required based on the value of a sibling field.","properties":{"type":{"type":"string","enum":["string","number","boolean","datetime","string_array"],"description":"Field value type."},"required":{"type":"boolean","description":"Whether the field must always be present."},"description":{"type":"string","description":"What the field means (for AI agents and workflow builders)."},"example":{"description":"An example value for this field."},"computeHint":{"type":"string","description":"How to extract this field from the raw source API."},"allowedValues":{"type":"array","items":{"type":"string"},"description":"Optional enum constraint."},"requiredWhen":{"type":"object","description":"Conditional requirement: required only when a sibling field has one of the listed values.","properties":{"field":{"type":"string","description":"Sibling field name to inspect."},"valueIn":{"type":"array","items":{"type":"string"},"minItems":1}},"required":["field","valueIn"]}},"required":["type","required","description"]},"AckdbContactMappingInput":{"type":"object","description":"Maps event content fields to a contact (person) extracted at ingest time. A durable external ID is required; display name, email, title, and other person properties are optional.","properties":{"role":{"type":"string","description":"Contact's role for this mapping (e.g. `user`, `attendee`)."},"nameField":{"type":"string","description":"Optional content field holding the contact's display name. Omit when a durable person key such as LinkedIn URL is the only person data this source owns."},"emailField":{"type":"string","description":"Content field holding the contact's email."},"titleField":{"type":"string","description":"Content field holding the contact's job title."},"externalIdField":{"type":"string","description":"Content field holding the contact's external ID value."},"externalIdType":{"type":"string","description":"External ID type for the contact (e.g. `app_user_email`)."},"linkedinUrlField":{"type":"string","description":"Content field holding the contact's LinkedIn URL."},"phoneField":{"type":"string","description":"Content field holding the contact's phone number."},"departmentField":{"type":"string","description":"Content field holding the contact's department."},"excludeWhen":{"type":"object","description":"Skip extracting this contact when these field/value conditions match.","additionalProperties":{}},"startedAtField":{"type":"string","description":"Content field holding when the EMPLOYMENT began (#345) — a fact, never inferred from ingest order."},"endedAtField":{"type":"string","description":"Content field holding when the employment ENDED (#345); a value flips the edge to `former`."},"externalIdScope":{"type":"string","enum":["person","affiliation"],"description":"What `externalIdField` identifies: the HUMAN (a durable person key) or the AFFILIATION (a workspace-local seat id, match-only). Default `affiliation`."},"endsPrior":{"type":"boolean","description":"This event asserts the human's OTHER affiliations have ended (job-change semantics): their other active edges flip to `former` with a `contact.ended` each. Person-scope event types only. Default false."},"isPrimary":{"type":"boolean","description":"The resolved edge becomes the person's PRIMARY affiliation (#544, `primarySource: ingest`; sticky while that edge is active). Applied only when the edge is ACTIVE. Person-scope event types only. Default false."}},"required":["role","externalIdField","externalIdType"]},"AckdbEventTypeInput":{"type":"object","description":"Event type definition payload for declaring/editing an event type. The same shape is accepted in `SourceCreateRequest.events[]`, `POST .../events`, and `PUT .../events/{eventType}`.","properties":{"eventType":{"type":"string","description":"Event type identifier (e.g. `usage.daily_rollup`)."},"description":{"type":"string","description":"What this event represents."},"category":{"type":"string","description":"Optional cross-source activity category."},"rationale":{"type":"string","description":"Optional: why this event is included."},"contentNature":{"type":"string","enum":["conversational","structured","activity-signal"],"description":"How the event's content should be treated downstream (e.g. for embedding)."},"eventScope":{"type":"string","enum":["person","entity","person_only"],"description":"entity: company; person: person at a company; person_only: independent human owner. Create a separate type to change ownership. Personal types support structured/activity content only."},"fields":{"type":"object","description":"Field name → field definition. The event's content schema.","additionalProperties":{"$ref":"#/components/schemas/AckdbEventTypeFieldInput"}},"promotedMetadataFields":{"type":"array","items":{"type":"string"},"description":"Fields promoted into entity_events.metadata for fast filtering/aggregation."},"traitHints":{"type":"array","description":"Optional traits that should be updated when this event is ingested.","items":{"type":"object","properties":{"traitKey":{"type":"string","description":"Trait to update. Must be a registered trait definition."},"computeHint":{"type":"string","description":"How to compute the trait value from this event's content."},"updateStrategy":{"type":"string","enum":["always","first_only","latest"],"description":"Advisory authoring hint only - it does NOT auto-translate to an ingest `op`. `always` ≈ `op:set`; `latest` ≈ `op:set_if_newer` (date traits); `first_only` has no native op (the workflow must guard on first observation). The workflow still sets the actual `op` on each ingested trait."}},"required":["traitKey"]}},"ingestExample":{"type":"object","description":"Optional complete POST /ingest template for this event."},"replacesEvents":{"type":"array","items":{"type":"string"},"description":"Source events intentionally excluded because this event covers their signal."},"contactMapping":{"$ref":"#/components/schemas/AckdbContactMappingInput"},"contactMappings":{"type":"array","items":{"$ref":"#/components/schemas/AckdbContactMappingInput"},"description":"Multiple contact mappings (merged with the singular `contactMapping`)."}},"required":["eventType","description","contentNature","eventScope","fields"]},"AckdbSourceCreateDefaultTrait":{"type":"object","description":"A default trait to register when declaring a source. The key MUST be `<sourceType>_*`.","properties":{"traitKey":{"type":"string","description":"Trait identifier, namespaced as `<sourceType>_*`.","example":"app_usage_dau"},"valueType":{"$ref":"#/components/schemas/AckdbTraitValueType"},"description":{"type":"string","description":"Human/AI-readable description of the trait."},"allowedValues":{"type":"array","items":{"type":"string"},"description":"Optional enum constraint."}},"required":["traitKey","valueType"]},"AckdbSourceCreateRequest":{"type":"object","description":"Declare a custom source with eventTypeField. identityLinkTypes may be empty for a closed source containing only person_only types; otherwise include domain.","properties":{"sourceType":{"type":"string","pattern":"^[a-z][a-z0-9_]*$","description":"Lowercase source identifier (e.g. `app_usage`).","example":"app_usage"},"displayName":{"type":"string","description":"Human-readable name.","example":"App Usage"},"description":{"type":"string","description":"What data this source carries."},"version":{"type":"string","description":"Optional source version string."},"queryingGuide":{"type":"string","description":"Optional agent-facing guidance on how to query this source's events."},"identityLinkTypes":{"type":"array","items":{"type":"string"},"minItems":0,"description":"May be empty when every declared event is person_only and allowCustomEventTypes is false. Otherwise must include domain. Add domain before adding company event types or allowing custom types.","example":["domain","app_account_id"]},"eventTypeField":{"type":"string","description":"REQUIRED. Which content.metadata field determines the event type.","example":"event_type"},"eventTypePrefix":{"type":"string","description":"Namespace prefix for event types.","example":"app_usage_"},"defaultContentNature":{"type":"string","enum":["conversational","structured","activity-signal"],"description":"Optional default content nature applied to events that don't specify one."},"allowCustomEventTypes":{"type":"boolean","description":"Optional HubSpot-custom-objects escape hatch: accept unknown event types and skip per-event validation."},"defaultTraits":{"type":"array","items":{"$ref":"#/components/schemas/AckdbSourceCreateDefaultTrait"},"description":"Optional default traits (each key must be `<sourceType>_*`)."},"events":{"type":"array","items":{"$ref":"#/components/schemas/AckdbEventTypeInput"},"description":"Optional event type definitions to create alongside the source."},"contextOnly":{"type":"boolean","description":"This source's events describe a PERSON but anchor to a company (`job` → the employer, `education` → the school), so the entities it mints are context rather than accounts (#493). A company whose only ingested evidence comes from such sources is hidden from the Companies grain and excluded from every score population. NULLABLE: `null` means never-decided and lets the pack loader seed it; `true`/`false` is a decision the loader will not overwrite."}},"required":["sourceType","displayName","description","identityLinkTypes","eventTypeField","eventTypePrefix"]},"AckdbSourceUpdateRequest":{"type":"object","description":"Partial source edit. Every field is optional; provided fields replace the existing ones. Allowed on custom sources and shipped packs (an edit marks the row `user_modified`).","properties":{"displayName":{"type":"string","description":"Human-readable name."},"description":{"type":"string","description":"What data this source carries."},"version":{"type":"string","description":"Source version string."},"queryingGuide":{"type":["string","null"],"description":"Agent-facing querying guidance. Pass null to clear it."},"identityLinkTypes":{"type":"array","items":{"type":"string"},"minItems":0,"description":"May be empty when every declared event is person_only and allowCustomEventTypes is false. Otherwise must include domain. Add domain before adding company event types or allowing custom types."},"eventTypeField":{"type":"string","description":"Which content.metadata field determines the event type."},"eventTypePrefix":{"type":"string","description":"Namespace prefix for event types."},"defaultContentNature":{"type":"string","enum":["conversational","structured","activity-signal"],"description":"Default content nature applied to events that don't specify one."},"allowCustomEventTypes":{"type":"boolean","description":"Toggle the custom-event-types escape hatch."},"defaultTraits":{"type":"array","items":{"$ref":"#/components/schemas/AckdbSourceCreateDefaultTrait"},"description":"Default traits (each key must be `<sourceType>_*`)."},"disabled":{"type":"boolean","description":"Whether the source rejects new ingest (422 SOURCE_DISABLED) without deleting its schema or history. Shipped packs ship `true` on a fresh deployment (opt-in per source); set `false` here to activate one."},"contextOnly":{"type":"boolean","nullable":true,"description":"This source's events describe a PERSON but anchor to a company (`job` → the employer, `education` → the school), so the entities it mints are context rather than accounts (#493). A company whose only ingested evidence comes from such sources is hidden from the Companies grain and excluded from every score population. NULLABLE: `null` means never-decided and lets the pack loader seed it; `true`/`false` is a decision the loader will not overwrite."}}},"AckdbTraitDefinition":{"type":"object","description":"Trait definition. Like Salesforce custom fields - defines what computed fields exist. AI agents discover fields by listing these at runtime.","properties":{"traitKey":{"type":"string","description":"Unique within org. Convention: prefix with source name.","example":"stripe_plan"},"valueType":{"$ref":"#/components/schemas/AckdbTraitValueType"},"source":{"type":"string","description":"Which source system this trait comes from.","example":"stripe"},"description":{"type":["string","null"],"description":"Human/AI-readable description. AI agents use this to understand the trait.","example":"Monthly recurring revenue in cents from Stripe"},"allowedValues":{"type":["array","null"],"description":"Optional enum constraint. Write API rejects values not in this list.","items":{},"example":null},"scope":{"type":"string","enum":["entity","contact"],"description":"Trait grain: entity (company) or contact. Defaults to \"entity\"."},"createdAt":{"type":"string","format":"date-time"}},"required":["traitKey","valueType","source","description","allowedValues","createdAt"]},"AckdbTraitValueType":{"type":"string","enum":["string","number","boolean","date","string_array"],"description":"Type system for trait validation and storage. Controls how trait values are validated on ingest and stored in the entity_traits table.","example":"number"},"AckdbIngestRequest":{"type":"object","description":"Request body for data ingestion from workflow tools. Content is validated against the source's content_schema, traits against trait_definitions. Ownership comes from the event type: entity is company-only, person is a person at a company, person_only is the human alone. Personal requests omit company links (or send empty arrays), forbid non-null entityId/contactId and nonempty contacts/traits/files, and resolve a durable person key or explicit personId. Personal structured/activity events create no affiliations.","properties":{"source":{"type":"string","description":"Source type (must exist in schema registry).","example":"slack"},"content":{"type":"object","description":"The content payload with text and source-specific metadata.","properties":{"text":{"type":"string","description":"Raw text from the source - this is what gets embedded and searched.","example":"Customer asked about pricing"},"metadata":{"type":"object","description":"Source-specific fields, validated against the source's content_schema.","additionalProperties":{}}},"required":["text","metadata"]},"identityLinks":{"type":"array","description":"At least one company link for entity/person types; omit or empty for person_only.","items":{"$ref":"#/components/schemas/AckdbIngestIdentityLink"},"minItems":0},"traits":{"type":"array","description":"Pre-computed trait values. Each is validated against trait_definitions.","items":{"$ref":"#/components/schemas/AckdbIngestTrait"}},"entityId":{"type":["string","null"],"description":"Explicit entity ID. If provided and no identity match, uses this entity.","example":null},"contactId":{"type":["string","null"],"description":"Optional strict affiliation pin for company-associated person events. Omit or null for person_only.","example":"01KWSVN7YY7Q2Q9QZ8GEHHYW14"},"personId":{"type":"string","description":"Optional strict human ULID pin. Personal events create no affiliation. Existing person-scope events retain company affiliation resolution.","example":"01KWSVN7YY7Q2Q9QZ8GEHHYW14"},"timestamp":{"type":"string","format":"date-time","description":"When this data was created in the source system (not when ingested).","example":"2026-03-08T10:00:00Z"},"idempotencyKey":{"type":"string","description":"Optional caller-provided idempotency key. If omitted, ackDB auto-computes one via SHA-256(source + content). Duplicate keys are silently accepted (200 with deduplicated: true). Use this to pass a natural key like a Stripe event ID or Slack message timestamp.","example":"stripe:evt_1234567890"},"contacts":{"type":"array","description":"Optional people to create or update at this entity, in addition to whatever the event's contact mappings extract. This is how a multi-person event (a meeting's attendee list, a group thread) records everyone: the contactId/personId rails above pin ONE actor, this array carries the rest. Each entry resolves independently through the same person-first resolver.","items":{"$ref":"#/components/schemas/AckdbIngestContact"}},"files":{"type":"array","description":"Optional files to upload and attach to this entity. Each file is base64-encoded. Max 10MB per file (configurable via FILE_MAX_SIZE_BYTES). Files are stored in the configured FileStore and processed asynchronously by `npm run process-files` to generate embeddings for semantic search.","items":{"$ref":"#/components/schemas/AckdbIngestFile"}}},"required":["source","content","timestamp"]},"AckdbIngestContact":{"type":"object","description":"One person to create or update at the resolved entity, pushed via IngestRequest.contacts. Resolution is person-first: durable keys (LinkedIn, then email, then person-scoped ids) find the human globally, then the affiliation edge at this company is found or created.","properties":{"contactId":{"type":"string","description":"Per-entry pin (#397). Maps this entry to an EXISTING contact edge instead of resolving it from keys — the answer when the caller already knows who this is but the payload carries no durable key the stored edge shares (a contact known only by slack_user_id, arriving from a source that only has an email). A strict assertion, never a mint: the edge must exist and belong to the resolved entity (422 otherwise), and a creation-grade key on this entry that contradicts the pinned person is a 409 with a merge candidate — ackDB never fuses two humans on a guess. Omit it and the entry resolves normally. Distinct from IngestRequest.contactId, which pins the EVENT'S ACTOR; a request may legitimately carry both, naming different people.","example":"01JQC7X8Y9Z0A1B2C3D4E5F6G7"},"email":{"type":"string","description":"Work email. A durable person key."},"name":{"type":"string","description":"Display name."},"title":{"type":"string","description":"Job title at this company."},"phone":{"type":"string","description":"Phone number."},"department":{"type":"string","description":"Department."},"linkedinUrl":{"type":"string","description":"LinkedIn profile URL — the strongest durable person key (tried before email), and the one that survives a job change."},"isExternal":{"type":"boolean","description":"Accepted for backwards compatibility and IGNORED. ackDB is the sole authority on internal/external classification, via the deployment's internal domains + internal users."},"identityLinks":{"type":"array","description":"Source-specific ids for this person. Universal-registered person-scope types join the global match ladder; everything else is stamped on the edge and is match-only.","items":{"type":"object","properties":{"externalIdType":{"type":"string","example":"slack_user_id"},"externalIdValue":{"type":"string","example":"U07ABC123"}},"required":["externalIdType","externalIdValue"]}},"traits":{"type":"array","description":"Contact-grain trait values (trait definitions with scope 'contact').","items":{"$ref":"#/components/schemas/AckdbIngestTrait"}}}},"AckdbIngestFile":{"type":"object","description":"A file to upload during ingest. Content is base64-encoded. Max 10MB.","properties":{"name":{"type":"string","description":"File name with extension.","example":"q4-proposal.pdf"},"type":{"type":"string","description":"MIME type of the file.","example":"application/pdf"},"sizeBytes":{"type":"integer","description":"File size in bytes. Must not exceed FILE_MAX_SIZE_BYTES (default 10MB = 10485760).","example":2048576},"content":{"type":"string","format":"byte","description":"Base64-encoded file content."},"sourceFileId":{"type":"string","description":"Original file ID from the source system (e.g., Slack file ID).","example":"F04ABC123"},"sourceUrl":{"type":"string","description":"Original download URL from the source system.","example":"https://files.slack.com/files-pri/T04ABC-F04ABC123/q4-proposal.pdf"},"metadata":{"type":"object","description":"Arbitrary source-specific metadata about the file.","additionalProperties":{}}},"required":["name","type","sizeBytes","content"]},"AckdbIngestIdentityLink":{"type":"object","description":"Identity link in an ingest request - maps an external ID to the data.","properties":{"externalIdType":{"type":"string","description":"Must be in the source definition's identityLinkTypes, or a universal type like `domain`.","example":"domain"},"externalIdValue":{"type":"string","description":"The actual external ID value.","example":"C04ABC"},"confidence":{"type":"number","description":"1.0 = verified/exact match, lower = fuzzy.","example":1,"minimum":0,"maximum":1}},"required":["externalIdType","externalIdValue","confidence"]},"AckdbIngestTrait":{"type":"object","description":"A pre-computed trait value in an ingest request.","properties":{"traitKey":{"type":"string","description":"Must exist in trait_definitions for this org.","example":"slack_message_count"},"value":{"description":"The trait value. Type must match the trait definition's valueType. **Do NOT send an empty string `\"\"` for a `number` or `date` trait - it is rejected (422). Omit the trait entirely when the source value is blank.**","example":42},"op":{"type":"string","description":"Atomic operation to apply. Default is `set` (overwrite). Use computed ops to avoid read-then-write race conditions on concurrent events.\n\n**Number ops:** `increment` (current + value), `decrement` (current - value), `min` (keep smaller), `max` (keep larger).\n\n**Date ops:** `set_if_newer` (keep more recent), `set_if_older` (keep older).\n\n**Array ops:** `append` (add items, deduped), `remove` (remove items).\n\n**The op MUST be compatible with the trait's `valueType` or the ingest is rejected (422):** `set` works for every type; `increment`/`decrement`/`min`/`max` are number-only; `set_if_newer`/`set_if_older` are date-only; `append`/`remove` are string_array-only. An omitted op silently defaults to `set` (overwrite) - pick the right op for accumulating counts/sums (`increment`) and 'keep latest' dates (`set_if_newer`), or you will clobber the prior value.","enum":["set","increment","decrement","min","max","set_if_newer","set_if_older","append","remove"],"default":"set","example":"increment"},"workflow":{"type":"string","description":"Which workflow computed this value. Empty string = direct API write.","example":"slack-ingestion"},"confidence":{"type":"number","description":"1.0 = observed/verified, lower = inferred/estimated.","example":1,"minimum":0,"maximum":1}},"required":["traitKey","value","workflow","confidence"]},"AckdbIngestResponse":{"type":"object","description":"Response from a successful ingest operation.","properties":{"data":{"type":"object","properties":{"entityId":{"type":["string","null"],"description":"Owning company; null for a sole-person event."},"contentId":{"type":"string","description":"ULID identifier for the stored raw payload record.","example":"01HXYZ123456789ABCDEFGI"},"created":{"type":"boolean","description":"True if a new entity was auto-created.","example":true},"identityLinksCreated":{"type":"integer","description":"Number of identity links created/updated.","example":1},"traitsWritten":{"type":"integer","description":"Number of traits written to storage.","example":1},"deduplicated":{"type":"boolean","description":"True if this request was a duplicate (same idempotency key already exists). No new raw payload, event, or unresolved record was created. Returns 200, not an error.","example":false},"fileIds":{"type":"array","items":{"type":"string"},"description":"IDs of files uploaded with this ingest request. Only present when files[] was included. Files are stored but not yet processed - run `npm run process-files` to generate embeddings."},"contactId":{"type":["string","null"],"description":"Affiliation actor; null for sole-person events."},"personId":{"type":["string","null"],"description":"Canonical human; non-null on successful synchronous personal writes."},"eventId":{"type":"string","description":"Persisted event ID, stable on personal retries."},"personsCreated":{"type":"integer"},"personsMatched":{"type":"integer"},"contactsProcessed":{"type":"integer"}},"required":["entityId","contentId","created","identityLinksCreated","traitsWritten"]}},"required":["data"]},"AckdbUnresolvedContent":{"type":"object","description":"Data that arrived via ingest but couldn't be linked to an entity. Stored separately, queryable, and resolvable via POST /entities/resolve.","properties":{"id":{"type":"string","description":"ULID identifier","example":"01HXYZ123456789ABCDEFGH"},"source":{"type":"string","example":"slack"},"identityLinks":{"type":"array","description":"Original identity links from the ingest request.","items":{"$ref":"#/components/schemas/AckdbIngestIdentityLink"}},"text":{"type":"string","description":"Raw text from the content payload."},"metadata":{"type":"object","description":"Source-specific metadata.","additionalProperties":{}},"traits":{"type":"array","description":"Original traits from the ingest request, replayed after resolution.","items":{"$ref":"#/components/schemas/AckdbIngestTrait"}},"rawPayload":{"type":"object","description":"Entire original request body."},"timestamp":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time"}},"required":["id","source","identityLinks","text","metadata","traits","rawPayload","timestamp"]},"AckdbResolveRequest":{"type":"object","description":"Request to resolve unresolved content to an entity.","properties":{"unresolvedContentId":{"type":"string","description":"ID of the unresolved content record to resolve.","example":"01HXYZ123456789ABCDEFGH"},"entityId":{"type":["string","null"],"description":"Target entity. Null = auto-create a new entity.","example":"01HXYZ123456789ABCDEFGI"}},"required":["unresolvedContentId","entityId"]},"AckdbResolveResponse":{"type":"object","description":"Response from a successful resolve operation.","properties":{"data":{"type":"object","properties":{"entityId":{"type":"string","example":"01HXYZ123456789ABCDEFGI"},"contentId":{"type":"string","example":"01HXYZ123456789ABCDEFGJ"},"created":{"type":"boolean","description":"True if a new entity was created.","example":false},"identityLinksCreated":{"type":"integer","example":1},"traitsWritten":{"type":"integer","example":2}},"required":["entityId","contentId","created","identityLinksCreated","traitsWritten"]}},"required":["data"]},"AckdbMergeRequest":{"type":"object","description":"Request to merge two duplicate entities. Winner keeps its entity_id.","properties":{"winnerId":{"type":"string","description":"Entity that survives the merge.","example":"01HXYZ123456789ABCDEFGH"},"loserId":{"type":"string","description":"Entity that gets soft-deleted after merge.","example":"01HABCD123456789EFGHIJK"}},"required":["winnerId","loserId"],"additionalProperties":false},"AckdbMergeResponse":{"type":"object","description":"Response from a successful merge operation. All data is transferred - nothing is lost.","properties":{"data":{"type":"object","properties":{"winnerId":{"type":"string","example":"01HXYZ123456789ABCDEFGH"},"loserId":{"type":"string","example":"01HABCD123456789EFGHIJK"},"linksTransferred":{"type":"integer","description":"Identity links moved to winner.","example":2},"traitsTransferred":{"type":"integer","description":"Traits re-assigned.","example":3},"contactsTransferred":{"type":"integer","description":"Contacts re-assigned.","example":1},"contactsMerged":{"type":"integer","description":"Loser contacts FOLDED into an existing winner contact because the same PERSON held an active edge at both companies (#325): one affiliation split across duplicate companies. Their events, links, traits and ownership move onto the surviving edge — nothing is dropped.","example":0},"eventsTransferred":{"type":"integer","description":"Entity events moved to winner.","example":42},"rawPayloadsTransferred":{"type":"integer","description":"Raw payloads moved to winner.","example":42},"contentTransferred":{"type":"integer","description":"Content chunks re-assigned.","example":5},"filesTransferred":{"type":"integer","description":"Files moved to winner.","example":0},"conversationsTransferred":{"type":"integer","description":"Conversations moved to winner.","example":3},"memberAssignmentsTransferred":{"type":"integer","description":"Member assignments moved or consolidated on the winner.","example":0},"relationshipsRepointed":{"type":"integer","example":1},"relationshipDuplicatesConsolidated":{"type":"integer","example":0},"selfRelationshipsRetracted":{"type":"integer","example":0}},"required":["winnerId","loserId","linksTransferred","traitsTransferred","contactsTransferred","contactsMerged","eventsTransferred","rawPayloadsTransferred","contentTransferred","filesTransferred","conversationsTransferred","memberAssignmentsTransferred","relationshipsRepointed","relationshipDuplicatesConsolidated","selfRelationshipsRetracted"]}},"required":["data"]},"AckdbEntity":{"type":"object","description":"A company/account entity in ackDB's entity store.","properties":{"entityId":{"type":"string","description":"Internal ULID identifier for the entity.","example":"01HXYZ123456789ABCDEFGH"},"name":{"type":["string","null"],"description":"Human-readable entity name (e.g. company name). May be null for auto-created entities.","example":"Acme Corp"},"createdAt":{"type":"string","format":"date-time","description":"When the entity was first created."},"updatedAt":{"type":"string","format":"date-time","description":"When the entity was last updated."}},"required":["entityId","name","createdAt","updatedAt"]},"AckdbIdentityLink":{"type":"object","description":"Maps an external ID (Slack channel, Stripe customer, etc.) to an entity. One entity can have many identity links across many sources.","properties":{"entityId":{"type":"string","description":"The entity this link belongs to.","example":"01HXYZ123456789ABCDEFGH"},"source":{"type":"string","description":"Source system (e.g. slack, stripe).","example":"slack"},"externalIdType":{"type":"string","description":"Type of external ID (e.g. slack_channel_id).","example":"slack_channel_id"},"externalIdValue":{"type":"string","description":"The actual external ID value.","example":"C04ABC"},"confidence":{"type":"number","description":"Confidence score of the link (0-1). 1.0 = verified.","example":1,"minimum":0,"maximum":1},"linkedAt":{"type":"string","format":"date-time","description":"When this identity link was created."}},"required":["entityId","source","externalIdType","externalIdValue","confidence","linkedAt"]},"AckdbContactIdentityLink":{"type":"object","description":"Maps an external ID to a contact (#310). One contact can have many links across many sources. Contact writes only accept REGISTERED universal link types (never per-source-declared types) — see POST /schemas/identity-link-types.","properties":{"contactId":{"type":"string","description":"The contact this link belongs to.","example":"01HXYZ123456789ABCDEFGH"},"source":{"type":"string","description":"Source system. Manual writes via this API are always stamped 'manual'.","example":"manual"},"externalIdType":{"type":"string","description":"Type of external ID (e.g. slack_user_id_custom). Never 'domain' — company-grain.","example":"slack_user_id_custom"},"externalIdValue":{"type":"string","description":"The actual external ID value.","example":"U123456"},"confidence":{"type":"number","description":"Confidence score of the link (0-1). 1.0 = verified.","example":1,"minimum":0,"maximum":1},"linkedAt":{"type":"string","format":"date-time","description":"When this identity link was created."}},"required":["contactId","source","externalIdType","externalIdValue","confidence","linkedAt"]},"AckdbIdentityLinkTypeDefinition":{"type":"object","description":"A universal identity link type (#310) — resolves/matches an entity from ANY source, not just the one that declared it (like `domain`, but tenant-defined). Match-only by default; an entity-scoped type with `canMint: true` (#451) is a secondary anchor that can also CREATE an entity at ingest, for companies that have no domain. `domain` itself is built-in (`builtIn: true`) and always mints.","properties":{"typeKey":{"type":"string","pattern":"^[a-z][a-z0-9_]*$","description":"Unique key. `domain` is exempt as the built-in and is not stored in this table.","example":"duns_number"},"description":{"type":["string","null"],"description":"Human/AI-readable description of what this identifier means.","example":"D-U-N-S business identifier"},"canMint":{"type":"boolean","description":"#451: whether this type may CREATE an entity at ingest (secondary anchor). false = match-only (the default). Always true on the built-in `domain` entry; never true on person-scoped types."},"builtIn":{"type":"boolean","description":"true only for the synthesized `domain` entry."},"createdAt":{"type":"string","format":"date-time","description":"Absent on the built-in `domain` entry."},"updatedAt":{"type":"string","format":"date-time","description":"Absent on the built-in `domain` entry."}},"required":["typeKey","description","builtIn"]},"AckdbIdentityLinkTypeCreateRequest":{"type":"object","description":"Request body for POST /schemas/identity-link-types.","properties":{"key":{"type":"string","pattern":"^[a-z][a-z0-9_]*$","description":"Trimmed, then validated. 'domain' is rejected (built-in).","example":"duns_number"},"description":{"type":["string","null"],"example":"D-U-N-S business identifier"},"canMint":{"type":"boolean","description":"#451: register as a secondary anchor that may CREATE an entity at ingest. Default false (match-only). Rejected with 400 for person-scoped types."}},"required":["key"]},"AckdbIdentityLinkTypeUpdateRequest":{"type":"object","description":"Request body for PATCH /schemas/identity-link-types/{typeKey}. Provide at least one field.","properties":{"description":{"type":["string","null"]},"canMint":{"type":"boolean","description":"#451: flip secondary-anchor minting. Turning it off is always safe (already-minted entities keep their links); turning it on is rejected with 400 for person-scoped types."}}},"AckdbAddIdentityLinkRequest":{"type":"object","description":"Request body for POST /entities/{ref}/links.","properties":{"externalIdType":{"type":"string","description":"A universal type (registered via POST /schemas/identity-link-types) or a source-declared type. Never 'domain'.","example":"duns_number"},"externalIdValue":{"type":"string","example":"123456789"},"source":{"type":"string","description":"Explicit source stamp. Optional for a universal type (defaults to 'manual'); required only when the type is declared by 2+ sources and needs disambiguating.","example":"manual"},"confidence":{"type":"number","description":"0-1. Defaults to 1.0.","minimum":0,"maximum":1,"example":1}},"required":["externalIdType","externalIdValue"]},"AckdbSetIdentityLinkValuesRequest":{"type":"object","description":"Request body for PUT /entities/{ref}/links/{type}. Declarative: this becomes the complete value set for the type; [] clears it.","properties":{"values":{"type":"array","items":{"type":"string"},"description":"Non-empty strings, deduplicated."},"source":{"type":"string","description":"Explicit source stamp for newly-added values only (see AddIdentityLinkRequest)."}},"required":["values"]},"AckdbAddContactIdentityLinkRequest":{"type":"object","description":"Request body for POST /contacts/{contactId}/links. Only a REGISTERED universal type is accepted (no source-declared types, no 'domain'); always stamped source:'manual'.","properties":{"externalIdType":{"type":"string","example":"slack_user_id_custom"},"externalIdValue":{"type":"string","example":"U123456"}},"required":["externalIdType","externalIdValue"]},"AckdbSetContactIdentityLinkValuesRequest":{"type":"object","description":"Request body for PUT /contacts/{contactId}/links/{type}. Declarative: this becomes the complete value set for the type; [] clears it. Adding requires a registered universal type; removing never does.","properties":{"values":{"type":"array","items":{"type":"string"},"description":"Non-empty strings, deduplicated."}},"required":["values"]},"AckdbEntityTrait":{"type":"object","description":"A computed trait value for an entity. Uses typed value columns - only one of valueString, valueNumber, valueBool, valueDate is populated based on the trait's type.","properties":{"entityId":{"type":"string","description":"The entity this trait belongs to.","example":"01HXYZ123456789ABCDEFGH"},"traitKey":{"type":"string","description":"Trait identifier (matches a trait_definition).","example":"slack_message_count"},"valueString":{"type":["string","null"],"description":"String value (populated for string and string_array traits).","example":null},"valueNumber":{"type":["number","null"],"description":"Numeric value (populated for number traits).","example":42},"valueBool":{"type":["boolean","null"],"description":"Boolean value (populated for boolean traits).","example":null},"valueDate":{"type":["string","null"],"format":"date-time","description":"Date value (populated for date traits).","example":null},"source":{"type":"string","description":"Source system that produced this trait.","example":"slack"},"workflow":{"type":"string","description":"Workflow that computed this value.","example":"slack-ingestion"},"confidence":{"type":"number","description":"Confidence score (0-1). 1.0 = observed/verified.","example":1,"minimum":0,"maximum":1},"updatedAt":{"type":"string","format":"date-time","description":"When this trait value was last updated."}},"required":["entityId","traitKey","valueString","valueNumber","valueBool","valueDate","source","workflow","confidence","updatedAt"]},"AckdbEntityEvent":{"type":"object","description":"Immutable company or human activity. Sole-person events have null entityId/contactId and a non-null personId, even when the human has employers.","properties":{"id":{"type":"string","description":"ULID identifier for the event.","example":"01HXYZ123456789ABCDEFGH"},"entityId":{"type":["string","null"]},"source":{"type":"string","description":"Source system that produced this event.","example":"slack"},"eventType":{"type":"string","description":"Type of event (e.g. message, payment, subscription_change).","example":"message"},"summary":{"type":"string","description":"Human-readable summary of the event.","example":"Customer asked about pricing in #acme-corp"},"metadata":{"type":"object","description":"Source-specific event metadata.","additionalProperties":{}},"timestamp":{"type":"string","format":"date-time","description":"When this event occurred in the source system."},"createdAt":{"type":"string","format":"date-time","description":"When this event record was created in ackDB."},"contactId":{"type":["string","null"]},"personId":{"type":["string","null"]}},"required":["id","entityId","source","eventType","summary","metadata","timestamp","createdAt","contactId","personId"]},"AckdbRawPayloadRecord":{"type":"object","description":"A raw source payload stored in Postgres. Used for L3 evidence tier - deep evidence, citations, and debugging.","properties":{"id":{"type":"string","description":"ULID identifier for the raw payload.","example":"01HXYZ123456789ABCDEFGH"},"source":{"type":"string","description":"Source system that produced this payload.","example":"slack"},"payload":{"type":"object","description":"The raw source payload as-is.","additionalProperties":{}},"createdAt":{"type":"string","format":"date-time","description":"When this payload was ingested."}},"required":["id","source","payload","createdAt"]},"AckdbPaginationMeta":{"type":"object","description":"Pagination metadata included in paginated responses.","properties":{"total":{"type":"integer","description":"Total number of records matching the query.","example":150},"limit":{"type":"integer","description":"Maximum number of records returned in this response.","example":50},"offset":{"type":"integer","description":"Number of records skipped.","example":0},"totalIsLowerBound":{"type":"boolean","description":"When true, `total` is a lower bound rather than an exact count. The Observe company query returns this when the count query was rejected or timed out and the result count was clamped (e.g. total=1000, totalIsLowerBound=true).","example":true}},"required":["total","limit","offset"]},"AckdbEntityProfile":{"type":"object","description":"Aggregated entity profile - everything known about an entity in a single object. This is the primary response format for AI agent queries (L1 tier).","properties":{"entity":{"$ref":"#/components/schemas/AckdbEntity"},"identityLinks":{"type":"array","items":{"$ref":"#/components/schemas/AckdbIdentityLink"},"description":"All identity links for this entity."},"traits":{"type":"array","items":{"$ref":"#/components/schemas/AckdbEntityTrait"},"description":"All computed traits for this entity."},"recentEvents":{"type":"array","items":{"$ref":"#/components/schemas/AckdbEntityEvent"},"description":"Recent activity events (default: last 20)."}},"required":["entity","identityLinks","traits","recentEvents"]},"AckdbPurgeResponse":{"type":"object","description":"Response from a purge operation. Includes per-table counts of deleted records so callers can verify the expected data was removed.","properties":{"data":{"type":"object","properties":{"message":{"type":"string","description":"Human-readable summary of the purge operation.","example":"Purged all data"},"source":{"type":"string","description":"Source that was purged (only present for source-scoped purges).","example":"stripe"},"deleted":{"type":"object","description":"Counts of deleted records per table.","properties":{"entities":{"type":"integer","description":"Entities deleted. For source-scoped purges, only entities orphaned by the purge (no remaining identity links and no data from any other source) are counted.","example":8},"contacts":{"type":"integer","description":"Contacts deleted (people belonging to a deleted entity).","example":24},"contactIdentityLinks":{"type":"integer","description":"Contact identity links deleted.","example":24},"contactTraits":{"type":"integer","description":"Contact trait values deleted.","example":48},"identityLinks":{"type":"integer","description":"Identity links deleted.","example":36},"traits":{"type":"integer","description":"Entity trait values deleted.","example":112},"events":{"type":"integer","description":"Entity events deleted.","example":112},"rawPayloads":{"type":"integer","description":"Raw payload records deleted.","example":112},"entityContent":{"type":"integer","description":"Entity content chunks (embeddings) deleted.","example":112},"unresolvedContent":{"type":"integer","description":"Unresolved content records deleted.","example":0},"unresolvedContacts":{"type":"integer","description":"Unresolved contact records deleted (people never linked to an entity).","example":0},"conversations":{"type":"integer","description":"Conversation records deleted.","example":0},"files":{"type":"integer","description":"Entity file records deleted.","example":0},"ingestQueue":{"type":"integer","description":"Queued ingest writes deleted (so a pending write cannot resurrect purged data).","example":0},"memberAssignments":{"type":"integer","description":"Member assignments deleted — ownership of a purged company/contact. The `internal_users` member roster itself is configuration and is preserved.","example":6},"agentSessionsDetached":{"type":"integer","description":"Agent sessions whose `entity_id` was set to NULL. Session history (transcripts, token + cost accounting) is preserved — only the pointer to the deleted entity is dropped.","example":3},"relationships":{"type":"integer","description":"Explicit company relationship episodes deleted by a full purge.","example":0},"contactRelationships":{"type":"integer","description":"Contact hierarchy episodes deleted by a full purge.","example":0}},"required":["entities","contacts","contactIdentityLinks","contactTraits","identityLinks","traits","events","relationships","contactRelationships","rawPayloads","entityContent","unresolvedContent","unresolvedContacts","conversations","files","ingestQueue","memberAssignments","agentSessionsDetached"]}},"required":["message","deleted"]}},"required":["data"]},"AckdbDeleteEventsRequest":{"type":"object","description":"Request body for DELETE /data/events. IDs-only by design (no filter selectors) to keep the blast radius minimal - look up ids via the read API first.","properties":{"ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":1000,"description":"Event ids (ULIDs) to delete. Duplicates are collapsed; unknown ids are reported in `notFound`.","example":["01J9ABCDEF0123456789ABCDEF","01J9ZYXWVU9876543210ZYXWVU"]},"dryRun":{"type":"boolean","default":false,"description":"When true, resolve and count what WOULD be deleted without mutating anything."},"reason":{"type":"string","maxLength":500,"description":"Optional reason, stored in the event_deletions audit row.","example":"duplicate Slack import"}},"required":["ids"]},"AckdbDeleteEventsResponse":{"type":"object","description":"Response from DELETE /data/events. A committed delete returns `deleted` (per-table counts); a dry-run returns `resolved` + `wouldDelete` with the identical report. `retained` says what was deliberately left standing; `pendingRebuild` reports, per entity+source pair, the derived content removed now and the surviving payloads queued for re-embedding.","properties":{"data":{"type":"object","properties":{"dryRun":{"type":"boolean","description":"Whether this was a preview (true) or a committed delete (false)."},"deleted":{"type":"object","description":"Per-table counts actually deleted (committed delete only).","properties":{"events":{"type":"integer","example":2},"rawPayloads":{"type":"integer","example":2},"files":{"type":"integer","example":1},"contentChunks":{"type":"integer","description":"Embedding chunks deleted — the affected pairs' message-derived chunks plus the deleted files' chunks (chunks a surviving file references are kept).","example":3},"conversations":{"type":"integer","description":"Conversation rows deleted because they contained a deleted payload.","example":1},"blobs":{"type":"integer","description":"File blobs deleted from the file store (best-effort, post-commit).","example":1}}},"wouldDelete":{"type":"object","description":"Per-table counts that WOULD be deleted (dry-run only). Same keys as `deleted`; `blobs` equals `files` (one stored blob per attached file).","properties":{"events":{"type":"integer","example":1},"rawPayloads":{"type":"integer","example":1},"files":{"type":"integer","example":0},"contentChunks":{"type":"integer","example":2},"conversations":{"type":"integer","example":1},"blobs":{"type":"integer","example":0}}},"resolved":{"type":"array","description":"Per-id resolution of the matched events (dry-run only).","items":{"type":"object","properties":{"id":{"type":"string"},"entityId":{"type":"string"},"source":{"type":"string"},"eventType":{"type":"string"},"summary":{"type":"string"},"timestamp":{"type":"string","format":"date-time"},"linkedRawPayloadId":{"type":"string","nullable":true},"fileCount":{"type":"integer"}}}},"notFound":{"type":"array","items":{"type":"string"},"description":"Requested ids that matched no event (reported, never fatal)."},"retained":{"type":"array","description":"What the delete deliberately left standing: `identity_links` are kept so a corrected re-ingest resolves to the same entity; an `entity` entry flags a company the delete left without source events/payloads (entity delete does not exist — the row stays, resolvable via its links).","items":{"type":"object","properties":{"kind":{"type":"string","enum":["identity_links","entity"]},"entityId":{"type":"string"},"count":{"type":"integer","description":"identity_links only — how many links the entity keeps."},"reason":{"type":"string"}},"required":["kind","entityId","reason"]}},"pendingRebuild":{"type":"array","description":"Per affected entity+source pair: derived content removed in this transaction and the surviving payloads queued for re-embedding. Until the embed/conversations pipelines next run, semantic search and conversation answers for the pair are missing — deliberately (missing is honest; stale is not).","items":{"type":"object","properties":{"entityId":{"type":"string"},"source":{"type":"string"},"contentChunksDeleted":{"type":"integer"},"conversationsDeleted":{"type":"integer"},"payloadsQueuedForReembed":{"type":"integer"}},"required":["entityId","source","contentChunksDeleted","conversationsDeleted","payloadsQueuedForReembed"]}},"rebuildNote":{"type":"string","description":"Present when pendingRebuild is non-empty — what is missing and how it comes back."}},"required":["dryRun","notFound","retained","pendingRebuild"]}},"required":["data"]},"AckdbAgentApiKeyStatus":{"type":"object","description":"Per-provider API key status. The raw key is write-only and never returned.","properties":{"configured":{"type":"boolean","description":"A usable key exists (runtime config or env var)."},"source":{"type":"string","enum":["custom","env","none"],"description":"custom = stored via PUT /config/agent; env = deployment env var; none = no key."},"masked":{"type":"string","nullable":true,"example":"sk-ant…abcd","description":"Masked preview (first 6 + last 4 chars) - only for runtime-configured keys."}}},"AckdbErrorResponse":{"type":"object","description":"Standard error response envelope.","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Machine-readable error code for programmatic handling.","example":"SOURCE_NOT_FOUND"},"message":{"type":"string","description":"Human-readable error message.","example":"Source type not found: unknown"}},"required":["code","message"]}},"required":["error"]},"AckdbConversation":{"type":"object","description":"A grouped, LLM-summarized conversation thread.","properties":{"id":{"type":"string","description":"ULID"},"entityId":{"type":"string"},"source":{"type":"string","example":"slack"},"sourceThreadKey":{"type":"string","description":"Source-specific thread key"},"messageCount":{"type":"integer"},"participantNames":{"type":"array","items":{"type":"string"}},"hasExternal":{"type":"boolean","description":"Whether external participants are present"},"sourceMetadata":{"type":"object","description":"Source-specific display fields (channel_name, etc.)"},"summary":{"type":"string","nullable":true,"description":"LLM-generated 1-3 sentence summary"},"topics":{"type":"array","items":{"type":"string"}},"sentiment":{"type":"string","nullable":true,"enum":["positive","negative","neutral","mixed"]},"actionItems":{"type":"array","items":{"type":"string"}},"fileIds":{"type":"array","items":{"type":"string"},"description":"IDs of entity_files attached to messages in this conversation"},"hasFiles":{"type":"boolean","description":"Whether this conversation has file attachments"},"firstMessageAt":{"type":"string","format":"date-time"},"lastMessageAt":{"type":"string","format":"date-time"}}},"AckdbIngestLog":{"type":"object","description":"Structured log for a single ingest HTTP request.","properties":{"id":{"type":"string","description":"ULID"},"endpoint":{"type":"string","example":"/ingest","description":"'/ingest' or '/ingest/batch'"},"source":{"type":"string","example":"slack"},"status":{"type":"string","enum":["success","deduplicated","unresolved","error"]},"entityId":{"type":"string","nullable":true},"entityCreated":{"type":"boolean"},"resolutionPath":{"type":"string","nullable":true,"enum":["domain_match","source_id_match","universal_id_match","domain_create","anchor_match","anchor_create","explicit","unresolved"]},"idempotencyKey":{"type":"string","nullable":true},"requestSizeBytes":{"type":"integer","nullable":true},"contentId":{"type":"string","nullable":true,"description":"raw_payload ID"},"eventId":{"type":"string","nullable":true,"description":"entity_event ID"},"identityLinksCreated":{"type":"integer"},"traitsWritten":{"type":"integer"},"contactsProcessed":{"type":"integer"},"filesProcessed":{"type":"integer"},"deduplicated":{"type":"boolean"},"itemCount":{"type":"integer","nullable":true,"description":"Batch only: total items in request"},"acceptedCount":{"type":"integer","nullable":true,"description":"Batch only: items accepted"},"duplicatedCount":{"type":"integer","nullable":true,"description":"Batch only: items deduplicated"},"errorCode":{"type":"string","nullable":true},"errorMessage":{"type":"string","nullable":true},"durationMs":{"type":"integer","description":"Total request duration in milliseconds"},"timingDetail":{"type":"object","nullable":true,"description":"Phase breakdown","properties":{"validation_ms":{"type":"integer"},"resolution_ms":{"type":"integer"},"writes_ms":{"type":"integer"}}},"createdAt":{"type":"string","format":"date-time"}}},"AckdbPagination":{"type":"object","properties":{"total":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"}}},"AckdbInternalUser":{"type":"object","description":"A known internal team member for contact classification.","properties":{"id":{"type":"string","description":"ULID"},"email":{"type":"string","nullable":true},"name":{"type":"string","nullable":true},"sourceIds":{"type":"array","items":{"type":"object","properties":{"source":{"type":"string"},"externalIdType":{"type":"string"},"externalIdValue":{"type":"string"}}}},"notes":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"}}},"AckdbFileMetadata":{"type":"object","description":"File metadata (summary view for list endpoints).","properties":{"id":{"type":"string","description":"ULID"},"entityId":{"type":"string"},"eventId":{"type":"string","nullable":true},"source":{"type":"string","example":"slack"},"fileName":{"type":"string","example":"q4-proposal.pdf"},"fileType":{"type":"string","description":"MIME type","example":"application/pdf"},"fileSizeBytes":{"type":"integer","example":2048576},"sourceFileId":{"type":"string","nullable":true},"sourceUrl":{"type":"string","nullable":true},"metadata":{"type":"object"},"description":{"type":"string","nullable":true,"description":"AI-generated description (after processing)"},"processed":{"type":"boolean","description":"Whether the file has been processed for embeddings"},"createdAt":{"type":"string","format":"date-time"}}},"AckdbFileMetadataDetail":{"type":"object","description":"Full file metadata including extracted text and content chunk link.","properties":{"id":{"type":"string"},"entityId":{"type":"string"},"eventId":{"type":"string","nullable":true},"source":{"type":"string"},"fileName":{"type":"string"},"fileType":{"type":"string"},"fileSizeBytes":{"type":"integer"},"sourceFileId":{"type":"string","nullable":true},"sourceUrl":{"type":"string","nullable":true},"metadata":{"type":"object"},"description":{"type":"string","nullable":true},"extractedText":{"type":"string","nullable":true,"description":"Text extracted from the file (PDFs, docs)"},"contentChunkId":{"type":"string","nullable":true,"description":"ID of the entity_content embedding chunk"},"processed":{"type":"boolean"},"processedAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"}}},"AckdbEntityLogoUpload":{"type":"object","description":"A base64-encoded company logo image.","properties":{"contentType":{"type":"string","enum":["image/png","image/jpeg","image/gif","image/webp","image/x-icon","image/vnd.microsoft.icon"],"description":"Image MIME type."},"dataBase64":{"type":"string","description":"Base64-encoded image bytes (<= 2MB decoded)."},"source":{"type":"string","description":"Optional provenance tag (e.g. \"explorium\"). Defaults to \"api\"."}},"required":["contentType","dataBase64"]},"AckdbSegmentChange":{"type":"object","description":"One change-log row: a member's transition for a segment.","properties":{"seq":{"type":"integer","description":"Global-monotonic log id. Advance your cursor to the max seq you have consumed."},"entityId":{"type":"string","description":"Member id - company entity_id or contact_id."},"kind":{"type":"string","enum":["enter","exit","reenter"]},"at":{"type":"string","format":"date-time","description":"When the row was appended."}},"required":["seq","entityId","kind","at"]},"AckdbSegmentChangesResponse":{"type":"object","properties":{"segmentId":{"type":"string"},"filterHash":{"type":"string","nullable":true,"description":"The membership's version key - changes iff a defining filter changed (detect an edit). Base segment: md5 of its filter. Derived segment: the composite `own+base`, so a base-filter edit flips it too. Null when edit detection is degraded (pre-migration DB) - null-propagating for derived, never a sentinel that looks like a real hash."},"maxSeq":{"type":"integer","description":"The segment's current highest seq. You are caught up when your last row's seq === maxSeq."},"minRetainedSeq":{"type":"integer","description":"Earliest seq still retained (prunedThroughSeq + 1)."},"changes":{"type":"array","items":{"$ref":"#/components/schemas/AckdbSegmentChange"}},"hasMore":{"type":"boolean","description":"More rows beyond this page - re-poll with since = nextSince."},"nextSince":{"type":"integer","description":"The cursor to poll with next (the last returned seq, or `since` if empty)."},"bootstrapped":{"type":"boolean","description":"Present + true on the first call (snapshot built, no rows returned; seed via /member-ids)."},"reconcile":{"type":"boolean","description":"Present + true when the cursor is unusable - re-seed via /member-ids (see `reason`)."},"reason":{"type":"string","enum":["cursor_ahead","cursor_expired"]}},"required":["segmentId","filterHash","maxSeq","minRetainedSeq","changes","hasMore","nextSince"]},"AckdbMemberIdsResponse":{"type":"object","properties":{"segmentId":{"type":"string"},"entityIds":{"type":"array","items":{"type":"string"},"description":"Member ids (company entity_id or contact_id), ascending."},"nextCursor":{"type":"string","nullable":true,"description":"Pass as ?after= for the next page; null when the set is exhausted."},"hasMore":{"type":"boolean"},"filterHash":{"type":"string","nullable":true,"description":"The membership's version hash (same value /changes carries). A multi-page seed that sees it change between pages straddles two populations and should restart. Null when edit detection is degraded."},"limit":{"type":"integer","description":"The effective page size (the request's `limit` after clamping to [1, 5000]; default 1000)."}},"required":["segmentId","entityIds","nextCursor","hasMore","limit"]},"AckdbMember":{"type":"object","description":"A member of the tenant's internal GTM team (the full internal_users row).","properties":{"id":{"type":"string","description":"ULID."},"email":{"type":"string","nullable":true},"name":{"type":"string","nullable":true},"title":{"type":"string","nullable":true,"description":"Function/role, e.g. Account Executive, SE, CSM."},"team":{"type":"string","nullable":true},"managerId":{"type":"string","nullable":true,"description":"Self-referential org hierarchy (a member id)."},"isActive":{"type":"boolean","description":"false = deactivated; excluded from active lookups."},"sourceIds":{"type":"array","description":"Deterministic source-id map (e.g. hubspot_owner_id, slack_user_id → member).","items":{"type":"object","properties":{"source":{"type":"string"},"externalIdType":{"type":"string"},"externalIdValue":{"type":"string"}}}},"notes":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","isActive","sourceIds","createdAt","updatedAt"]},"AckdbMemberAssignment":{"type":"object","description":"The ownership link between a member and an owned company (entityId) or contact (contactId) — exactly one target is set. isPrimary marks the owner/DRI.","properties":{"id":{"type":"string","description":"ULID."},"memberId":{"type":"string"},"entityId":{"type":"string","nullable":true,"description":"Company target; null for a contact-target assignment."},"contactId":{"type":"string","nullable":true,"description":"Contact target — relationship ownership (#253). Null for a company-target assignment."},"isPrimary":{"type":"boolean","description":"true = the account owner/DRI (at most one active primary per company)."},"status":{"type":"string","enum":["active","removed"]},"source":{"type":"string","description":"Provenance: manual | hubspot | system | …"},"confidence":{"type":"number","description":"1.0 = explicit, lower = inferred."},"assignedAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","memberId","isPrimary","status","source","confidence","assignedAt","updatedAt"]},"AckdbMemberCreateRequest":{"type":"object","description":"Create a member. Supply at least one of email, name, or sourceIds.","properties":{"email":{"type":"string"},"name":{"type":"string"},"title":{"type":"string"},"team":{"type":"string"},"managerId":{"type":"string"},"notes":{"type":"string"},"sourceIds":{"type":"array","items":{"type":"object","properties":{"source":{"type":"string"},"externalIdType":{"type":"string"},"externalIdValue":{"type":"string"}},"required":["source","externalIdType","externalIdValue"]}}}},"AckdbMemberUpdateRequest":{"type":"object","description":"Patch a member. Only provided fields change; nullable fields accept null to clear.","properties":{"email":{"type":"string","nullable":true},"name":{"type":"string","nullable":true},"title":{"type":"string","nullable":true},"team":{"type":"string","nullable":true},"managerId":{"type":"string","nullable":true},"isActive":{"type":"boolean"},"notes":{"type":"string","nullable":true},"sourceIds":{"type":"array","items":{"type":"object","properties":{"source":{"type":"string"},"externalIdType":{"type":"string"},"externalIdValue":{"type":"string"}}}}}},"AckdbMemberAssignRequest":{"type":"object","description":"Assign or reassign a member to a company or contact. Identify the member by memberId OR email.","properties":{"memberId":{"type":"string","description":"The member id (mutually exclusive with email)."},"email":{"type":"string","description":"The member email — resolves-or-creates the member."},"name":{"type":"string","description":"Used only when creating a member by a new email."},"title":{"type":"string","description":"Used only when creating a member by a new email."},"isPrimary":{"type":"boolean","description":"true = make them the owner/DRI (flips the current owner). Default false."},"source":{"type":"string","description":"Provenance tag for the assignment (default 'manual')."}}},"AckdbBulkAssignRequest":{"type":"object","description":"Bulk-assign one member to many targets (#252). Supply at least one of entityIds, domains, or contactIds. isPrimary applies to every target.","properties":{"entityIds":{"type":"array","items":{"type":"string"},"description":"Company entity ids (ULID)."},"domains":{"type":"array","items":{"type":"string"},"description":"Company domains (resolved via the identity map)."},"contactIds":{"type":"array","items":{"type":"string"},"description":"Contact ids (relationship ownership)."},"isPrimary":{"type":"boolean","description":"true = owner/DRI on every target. Default false."},"source":{"type":"string","description":"Provenance tag (default 'manual')."}}},"AckdbBulkUnassignRequest":{"type":"object","description":"Bulk-unassign one member from many explicit targets (#252). Supply at least one of entityIds, domains, or contactIds.","properties":{"entityIds":{"type":"array","items":{"type":"string"},"description":"Company entity ids (ULID)."},"domains":{"type":"array","items":{"type":"string"},"description":"Company domains."},"contactIds":{"type":"array","items":{"type":"string"},"description":"Contact ids."}}},"AckdbTransferBookRequest":{"type":"object","description":"Transfer a member's book (companies + contacts) to another member (#252). Destination by toMemberId OR toEmail. all:true moves the whole book; otherwise list entityIds/contactIds.","properties":{"toMemberId":{"type":"string","description":"Destination member id (mutually exclusive with toEmail)."},"toEmail":{"type":"string","description":"Destination member email — resolves-or-creates."},"name":{"type":"string","description":"Used only when creating the destination member by a new email."},"title":{"type":"string","description":"Used only when creating the destination member by a new email."},"entityIds":{"type":"array","items":{"type":"string"},"description":"Subset of companies to move."},"contactIds":{"type":"array","items":{"type":"string"},"description":"Subset of contacts to move."},"all":{"type":"boolean","description":"true = move the member's entire active book."},"source":{"type":"string","description":"Provenance tag (default 'manual')."}}},"AckdbAttributeDefinition":{"type":"object","description":"An attribute definition (standard or custom) — a trait_definitions row shaped for the Attributes API.","properties":{"key":{"type":"string"},"label":{"type":"string","description":"Display label — a field_label_overrides value, or a prettified key."},"scope":{"type":"string","enum":["company","contact","person"],"description":"What the fact is ABOUT: company = the account; person = the HUMAN (survives job changes); contact = one person-at-one-company affiliation."},"type":{"type":"string","enum":["string","number","boolean","date","string_array"]},"origin":{"type":"string","enum":["standard","custom"],"description":"\"standard\" = shipped by ackDB (never deletable; only its enum is editable, see editableFields); \"custom\" = user-created via POST /attributes."},"description":{"type":"string","nullable":true,"description":"Read by the AI agent for semantic understanding."},"allowedValues":{"type":"array","nullable":true,"items":{"type":"string"},"description":"Enum constraint (string type only)."},"editable":{"type":"boolean","description":"Always true — every attribute's values may be set."},"editableFields":{"type":"array","items":{"type":"string","enum":["label","description","allowedValues","scope"]},"description":"Which DEFINITION fields PATCH accepts (#487) — the authoritative list, so a client never has to infer the rules. Custom: label + description (+ allowedValues when type is \"string\", + scope when contact-scope). Standard: `[\"allowedValues\"]` when it ships an enum, otherwise empty (read-only)."},"deletable":{"type":"boolean","description":"true only for custom attributes."},"userModified":{"type":"boolean","description":"This deployment has edited the definition, so the boot loader no longer re-asserts the shipped metadata (#487). On a standard attribute it means the tenant owns this enum now."},"valueCount":{"type":"number","description":"Active records currently holding a value."},"createdAt":{"type":"string","format":"date-time"}},"required":["key","label","scope","type","origin","editable","editableFields","deletable","userModified","valueCount","createdAt"]},"AckdbAttributeCreateRequest":{"type":"object","description":"Create a custom attribute definition.","properties":{"key":{"type":"string","description":"snake_case, ^[a-z][a-z0-9_]{0,63}$."},"label":{"type":"string","description":"Display label; defaults to a prettified key."},"scope":{"type":"string","enum":["company","contact","person"],"description":"What the fact is ABOUT. Prefer `person` for facts about a HUMAN (is_champion, do_not_contact) — they survive job changes and render on the People view. `contact` scopes the fact to ONE employment and is API-only: no console grain renders it."},"type":{"type":"string","enum":["string","number","boolean","date","string_array"]},"description":{"type":"string","nullable":true,"description":"Read by the AI agent for semantic understanding."},"allowedValues":{"type":"array","nullable":true,"items":{"type":"string"},"description":"Enum constraint — string type only, non-empty if present."}},"required":["key","scope","type"]},"AckdbAttributePatchRequest":{"type":"object","description":"Edit an attribute's metadata (any subset of the fields its `editableFields` lists). `key` and `type` are always immutable. A custom attribute takes label/description/allowedValues; a STANDARD one takes `allowedValues` alone (#487). `scope` is the exclusive contact → person migration (#474) and must be the only field in the request.","properties":{"label":{"type":"string"},"description":{"type":"string","nullable":true},"allowedValues":{"type":"array","nullable":true,"items":{"type":"string"}},"scope":{"type":"string","enum":["person"],"description":"Scope migration (#474) — only \"person\"."},"dryRun":{"type":"boolean","description":"Scope migration only: run the full cascade and roll back."}}},"AckdbAttributePutValuesRequest":{"type":"object","description":"Set/clear attribute values on one record. `null` clears a value (DELETEs the row). All-or-nothing: every key/value is validated before anything is written.","properties":{"values":{"type":"object","description":"Map of attribute key → new value (or null to clear). Must be non-empty.","additionalProperties":true},"actor":{"type":"string","description":"Who made the change — recorded as the value row's updatedBy and the timeline event's actor (person.attribute.changed for people, trait.updated for companies/contacts). Defaults to \"api\". Observe/agent write paths stamp the real user automatically."}},"required":["values"]},"AckdbPerson":{"type":"object","description":"One human. The true identity is the ULID; durable keys are pointers into it (exactly as `domain` → entityId). `status` is the IDENTITY lifecycle only — a person with three ended jobs is still `active`; employment state lives on each contact edge.","properties":{"personId":{"type":"string","example":"01J8ZC2V9K3W4X5Y6Z7A8B9C0D"},"name":{"type":"string","nullable":true,"example":"Elly Chen"},"status":{"type":"string","enum":["active","merged"]},"mergedInto":{"type":"string","nullable":true,"description":"The surviving person after a merge (tombstone chain); null while active."},"avatarUrl":{"type":"string","nullable":true,"description":"Photo URL — a fact about the human, projected onto every contact read. http(s) only; ackDB stores the link, never the bytes (the frontend hotlinks it). Latest non-null write wins.","example":"https://media.licdn.com/dms/image/v2/abc/profile.jpg"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"primaryContactId":{"type":"string","nullable":true,"description":"The person's PRIMARY affiliation edge (#544) — the one contact row a People table shows for them. Picked by a rule over their active edges (account company before reference company, dated current job first, then most recently seen), kept current by a trigger on contacts, overridable via PATCH. Null when the human has no active edge at an active company.","example":"01J8ZD3W0L4X5Y6Z7A8B9C0D1E"},"primarySource":{"type":"string","enum":["rule","manual","ingest"],"description":"Who picked `primaryContactId`: `rule` (default), `manual` (PATCH / inline edit) or `ingest` (a contact mapping flagged `isPrimary`). `manual` and `ingest` stick while that edge stays active."}},"required":["personId","status","primarySource","createdAt","updatedAt"]},"AckdbPersonListItem":{"type":"object","description":"A Person plus the cheap list-view aggregates (no N+1 needed to render a table).","allOf":[{"$ref":"#/components/schemas/AckdbPerson"}],"properties":{"affiliationCount":{"type":"integer","description":"Contact edges (any status).","example":2},"keyCount":{"type":"integer","description":"Durable keys.","example":3},"isInternal":{"type":"boolean","description":"True when any edge marks them internal (is_external = false) — the member firewall stays intact."}}},"AckdbPersonIdentityLink":{"type":"object","description":"A durable person key. Values are stored NORMALIZED and are globally UNIQUE per (externalIdType, externalIdValue) — one human per key, which is the structural dedup guarantee. Keys accrete over a career and are never re-pointed (first-writer-wins).","properties":{"personId":{"type":"string"},"source":{"type":"string","description":"Source system that asserted this key.","example":"emails"},"externalIdType":{"type":"string","description":"`linkedin_url` (canonical anchor), `email_address` (provisional), or a person-scoped id type.","example":"linkedin_url"},"externalIdValue":{"type":"string","description":"The normalized value (lowercased email; canonical `linkedin.com/in/<slug>`).","example":"linkedin.com/in/ellychen"},"confidence":{"type":"number","example":1},"evidencePayloadId":{"type":"string","nullable":true,"description":"raw_payloads.id of the ingest that asserted this key (null for manual writes)."},"linkedAt":{"type":"string","format":"date-time"}},"required":["personId","source","externalIdType","externalIdValue","confidence","linkedAt"]},"AckdbPersonAffiliation":{"type":"object","description":"One STINT: a contact edge (person @ company) plus its company name. `status: former` + `endedAt` is a finished stint; a boomerang employee has several stints at the same company.","properties":{"contact":{"type":"object","description":"The full contact row (carries personId, status, endedAt, title …)."},"entityName":{"type":"string","nullable":true,"example":"Acme Corp"}},"required":["contact"]},"AckdbPersonProfile":{"type":"object","description":"The person grain's L1 read: who they are, how we know them, where they work(ed), what just happened.","properties":{"person":{"$ref":"#/components/schemas/AckdbPerson"},"keys":{"type":"array","items":{"$ref":"#/components/schemas/AckdbPersonIdentityLink"}},"affiliations":{"type":"array","items":{"$ref":"#/components/schemas/AckdbPersonAffiliation"},"description":"Stints, active first — CAPPED by contactLimit."},"totalAffiliations":{"type":"integer","description":"True edge count (tells you the list above was capped)."},"recentEvents":{"type":"array","items":{"type":"object"},"description":"Most recent events on the person timeline, interleaved across companies."}},"required":["person","keys","affiliations","totalAffiliations","recentEvents"]},"AckdbMergeCandidate":{"type":"object","description":"One first-writer-wins key conflict: a write tried to attach a key that another person already owns. It is recorded, never acted on — merging is always a human decision. A repeat of the same tuple bumps `updatedAt`; a dismissed candidate stays dismissed.","properties":{"id":{"type":"string"},"personA":{"type":"string","description":"The person the conflicting write resolved to."},"personB":{"type":"string","description":"The current owner of the contested key."},"personAName":{"type":"string","nullable":true},"personBName":{"type":"string","nullable":true},"keyType":{"type":"string","example":"email_address"},"keyValue":{"type":"string","example":"elly@acme.com"},"evidencePayloadId":{"type":"string","nullable":true,"description":"The ingest that produced the conflict."},"source":{"type":"string","example":"slack"},"status":{"type":"string","enum":["open","merged","dismissed"]},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time","description":"Bumped on every repeat of the same conflict."}},"required":["id","personA","personB","keyType","keyValue","source","status","createdAt","updatedAt"]},"AckdbPersonMergeResult":{"type":"object","description":"Outcome of a person merge. `merged: false` = both ids already resolved to the same human (a no-op).","properties":{"winnerId":{"type":"string"},"loserId":{"type":"string","description":"Equals winnerId on a no-op."},"merged":{"type":"boolean"},"keysMoved":{"type":"integer"},"contactsRepointed":{"type":"integer","description":"Loser edges moved to the winner as-is."},"contactMerges":{"type":"integer","description":"Loser edges folded into a winner edge at the same company."},"eventsRestamped":{"type":"integer","description":"entity_events rows re-attributed to the winner."},"candidatesResolved":{"type":"integer"},"mergeEventEmitted":{"type":"boolean","description":"True for a completed merge: person.identity.merged is emitted on the winner with no company/contact. False for an already-merged no-op."}},"required":["winnerId","loserId","merged"]},"AckdbPersonCreateRequest":{"type":"object","description":"Manual person create. All fields are optional — a person with no keys is a legal singleton.","properties":{"name":{"type":"string","nullable":true,"example":"Elly Chen"},"avatarUrl":{"type":"string","nullable":true,"description":"Photo URL (http(s) only). ackDB stores the link, never the bytes.","example":"https://media.licdn.com/dms/image/v2/abc/profile.jpg"},"keys":{"type":"array","description":"Durable keys to claim. Person-scope types only; values are normalized and validated.","items":{"type":"object","properties":{"externalIdType":{"type":"string","example":"email_address"},"externalIdValue":{"type":"string","example":"elly@newco.com"}},"required":["externalIdType","externalIdValue"]}}}},"AckdbPersonUpdateRequest":{"type":"object","description":"Person-grain fact update (#372, #544). At least one field must be provided.","properties":{"avatarUrl":{"type":"string","nullable":true,"description":"Photo URL (http(s) only, ≤2048 chars). `null` clears it.","example":"https://media.licdn.com/dms/image/v2/abc/profile.jpg"},"primaryContactId":{"type":"string","nullable":true,"description":"The person's PRIMARY affiliation (#544). A contact id makes that edge primary (`primarySource: manual`, sticky while the edge is active; must be this person's ACTIVE edge at an active company → 400, unknown → 404). `null` clears the override and lets the rule re-pick. Emits `contact.primary_changed` when the pointer moves.","example":"01J8ZD3W0L4X5Y6Z7A8B9C0D1E"}}},"AckdbPersonLinkCreateRequest":{"type":"object","description":"Attach one durable key to a person.","properties":{"externalIdType":{"type":"string","example":"linkedin_url"},"externalIdValue":{"type":"string","example":"https://www.linkedin.com/in/ellychen/"},"source":{"type":"string","description":"Source stamp on the key row. Defaults to \"manual\"."}},"required":["externalIdType","externalIdValue"]},"AckdbPersonMergeRequest":{"type":"object","description":"Merge `loserId` into the path person (or into `winnerOverride`, which must be one of the two).","properties":{"loserId":{"type":"string","example":"01HABCD123456789EFGHIJK"},"winnerOverride":{"type":"string","nullable":true}},"required":["loserId"]},"AckdbMergeDuplicatePersonsRequest":{"type":"object","description":"Batch-consume the open merge candidates. Dry-run unless `dryRun: false` is sent explicitly.","properties":{"dryRun":{"type":"boolean","default":true},"limit":{"type":"integer","default":50,"maximum":200,"description":"How many open candidates to consider."}}},"AckdbContactCreateRequest":{"type":"object","description":"Explicit contact push. `linkedinUrl` / `email` double as the person's durable keys (that is how the human is resolved). `isExternal` is accepted but IGNORED — ackDB is the sole authority on internal/external classification (deployment config).","properties":{"email":{"type":"string","example":"elly@acme.com"},"name":{"type":"string","example":"Elly Chen"},"title":{"type":"string","example":"VP Engineering"},"phone":{"type":"string"},"department":{"type":"string"},"linkedinUrl":{"type":"string","example":"https://www.linkedin.com/in/ellychen"},"avatarUrl":{"type":"string","nullable":true,"description":"Photo URL — written to the resolved PERSON (http(s) only). `null` clears it.","example":"https://media.licdn.com/dms/image/v2/abc/profile.jpg"},"source":{"type":"string","default":"api"},"identityLinks":{"type":"array","items":{"type":"object","properties":{"externalIdType":{"type":"string"},"externalIdValue":{"type":"string"}},"required":["externalIdType","externalIdValue"]}},"traits":{"type":"array","items":{"type":"object"},"description":"Contact traits (EAV)."}}},"AckdbContactUpdateRequest":{"type":"object","description":"Any subset. `title` and `status` transitions emit timeline events (see the endpoint description).","properties":{"name":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string"},"title":{"type":"string"},"department":{"type":"string"},"linkedinUrl":{"type":"string"},"avatarUrl":{"type":"string","nullable":true,"description":"Photo URL — written to the edge's PERSON (http(s) only). `null` clears it."},"isExternal":{"type":"boolean"},"status":{"type":"string","enum":["active","former","disabled"],"description":"`former` ends the affiliation (stamps endedAt + emits contact.ended)."},"endedAt":{"type":"string","format":"date-time","description":"When the affiliation ended. Only read when status becomes `former`; defaults to now."}}},"AckdbContactMoveRequest":{"type":"object","description":"Re-parent this edge to the company it really belongs to (a correction — its events move with it).","properties":{"entityId":{"type":"string","description":"The correct company.","example":"01J8ZE4X1M5Y6Z7A8B9C0D1E2F"},"reason":{"type":"string","description":"Free-form operator note, recorded on the contact.moved event."}},"required":["entityId"]},"AckdbContactRepointRequest":{"type":"object","description":"Re-point this edge to another human (a correction — its events are restamped with the new person).","properties":{"personId":{"type":"string","description":"The person this edge really belongs to."}},"required":["personId"]},"AckdbScoreCriterion":{"type":"object","description":"One scoring rule: a field reference + exactly one scoring mode. Field refs — `trait:<key>` / `attribute:<key>` (company grain; person grain: `attribute:is_internal|affiliation_count|key_count`), `identity_link:<type>`, `event:<source>.<bareType>` (aggregate), `contacts` (company grain persona count), `company:trait:<key>` (person grain, best match over ANY active affiliation), `convergence` (both grains: the count of DISTINCT `sources` with activity — or of DISTINCT fired `signals` event refs — inside `windowDays`, scored via `bands`; exactly one of sources | signals).","properties":{"group":{"type":"string","description":"Key of the group this criterion belongs to."},"field":{"type":"string","example":"event:slack.message"},"label":{"type":"string","description":"Optional display label for explain/UI."},"match":{"type":"array","description":"Value → points entries (first match wins). op: eq | in | present. Legal on value-shaped refs (trait/attribute/identity_link/company:) and, since #438, on `agg: \"latest\"` event refs: the one aggregate that reduces to a VALUE rather than a volume, so it compares the `valueField` as text where bands/perUnitPoints cast it to numeric. Counting aggregates (count/sum/distinct_contacts) and `contacts` reject match (400), since comparing a count against numbers is what bands is for. Exactly one of match | bands | perUnitPoints(+cap) per criterion. On event metadata the comparison is EXACT and case-sensitive (see /scores/capabilities, `eventValueMatching`): trait values canonicalize case-insensitively (#380), event fields deliberately do not, so seed value pickers from /fields/{key}/values instead of accepting free text.","items":{"type":"object","properties":{"op":{"type":"string","enum":["eq","in","present"]},"values":{"type":"array","items":{}},"points":{"type":"number","minimum":-100,"maximum":100}},"required":["op","points"]}},"bands":{"type":"array","description":"Numeric threshold bands (first match wins). Exactly one of gte/lt per band. Points are ±100, the same bound match entries and perUnitPoints carry.","items":{"type":"object","properties":{"gte":{"type":"number"},"lt":{"type":"number"},"points":{"type":"number","minimum":-100,"maximum":100}},"required":["points"]}},"agg":{"type":"string","enum":["count","sum","distinct_contacts","latest"],"description":"Aggregate reducer (event: refs; default count). distinct_contacts is company-grain only. latest (#424) scores the CURRENT value carried in event metadata — the `valueField` of the most recent matching event — instead of a volume."},"sumField":{"type":"string","description":"Promoted NUMERIC metadata field — required when agg=sum."},"valueField":{"type":"string","description":"Promoted metadata field read off the most recent matching event — REQUIRED when agg=latest, rejected otherwise (#424). Must be promoted on the criterion's event type (entity_events.metadata holds promoted fields only). Its value is cast to numeric; a non-numeric value scores 0 points.","example":"employee_count"},"windowDays":{"type":"integer","minimum":1,"maximum":3650,"description":"Trailing window; absent = all-time. REQUIRED on convergence, with a tighter 1–365 bound."},"halfLifeDays":{"type":"integer","minimum":1,"maximum":3650,"description":"Exponential decay half-life (weight = 2^(−age/halfLife)). Not with distinct_contacts or latest."},"perUnitPoints":{"type":"number","minimum":-100,"maximum":100,"description":"Linear points per unit (with cap)."},"cap":{"type":"number","description":"Symmetric clamp for perUnitPoints (0 < cap ≤ 100). Required with it."},"filter":{"type":"object","description":"contacts criterion only: persona filter.","properties":{"titleContains":{"type":"array","items":{"type":"string"}},"activeWithinDays":{"type":"integer"}}},"sources":{"type":"array","items":{"type":"string"},"minItems":2,"description":"convergence criterion only: the distinct sources counted (≥2, each must exist in the schema registry). Double counting with per-source `event:` criteria is intended — convergence is the multiplier on top of per-source activity. Exactly one of `sources` | `signals`."},"signals":{"type":"array","items":{"type":"string"},"minItems":2,"maxItems":20,"description":"convergence criterion only (#421): user-composed signal list — `event:<source>.<bareType>` refs (2–20, deduped, each must resolve in the schema registry; person grain needs person-scope types). The criterion value is how many listed signals had ≥1 event in the window — a signal firing 40× counts once. Exactly one of `sources` | `signals`."}},"required":["group","field"]},"AckdbScoreDefinitionInput":{"type":"object","description":"Create/replace body for a score definition.","properties":{"scoreKey":{"type":"string","pattern":"^[a-z][a-z0-9]*(_[a-z0-9]+)*$","maxLength":40,"description":"Immutable slug → the sc__<scoreKey> column. Single underscores only.","example":"pql"},"name":{"type":"string","example":"PQL score"},"description":{"type":"string","nullable":true},"grain":{"type":"string","enum":["company","person"],"description":"Immutable after create."},"groups":{"type":"array","description":"Weighted criterion groups; weights must sum to 1 (±0.01).","items":{"type":"object","properties":{"key":{"type":"string","example":"fit"},"weight":{"type":"number","minimum":0,"maximum":1,"example":0.4}},"required":["key","weight"]}},"criteria":{"type":"array","items":{"$ref":"#/components/schemas/AckdbScoreCriterion"}},"normalization":{"type":"string","enum":["absolute","percentile"],"description":"absolute (default): 100 × points / max possible, clamped. percentile: rank within THIS grain's population (strictly-below semantics — a no-data row is the 0th percentile)."},"percentilePopulation":{"type":"string","enum":["all","active"],"description":"Percentile definitions only (#432) — 400 with absolute normalization. all (default): rank against the whole grain population (today's behavior). active: rank only against rows where at least one non-trait criterion fired (an event:/convergence/contacts/company: criterion contributed nonzero points) — on sparse books the empty rows stop being the yardstick. Excluded rows still serve a score of 0 (never NULL) and take whatever lt label band covers 0."},"labels":{"type":"array","description":"Label bands over the normalized 0–100 score (first match wins).","items":{"type":"object","properties":{"gte":{"type":"number"},"lt":{"type":"number"},"label":{"type":"string","example":"Excellent"}},"required":["label"]}},"scopeSegmentId":{"type":"string","nullable":true,"description":"SCOPE (#443): score ONLY the members of this saved segment. Absent/null = the whole grain (every definition before this feature). Rows outside the segment read NULL in sc__<key> and sc__<key>__label — not 0 — and are excluded from the percentile population, the preview histogram and the history sweep. ANY segment grain is legal for either score grain: a contact/person segment scoping a company score means 'the companies of those people', a company segment scoping a person score means 'the people with an active edge at those companies'. Derived segments are legal. 400s when the segment does not exist, or when its filter (or its base's) references a SCOPED score — a scored population may only be defined by whole-grain scores, which is what makes a scoring cycle impossible."},"isPrimary":{"type":"boolean","description":"One primary per grain (server-enforced hand-off)."},"enabled":{"type":"boolean","default":true}},"required":["scoreKey","name","grain","groups","criteria"]},"AckdbScoreDefinition":{"allOf":[{"$ref":"#/components/schemas/AckdbScoreDefinitionInput"},{"type":"object","properties":{"degraded":{"type":"boolean","description":"True when a source deletion scrubbed criteria out of this definition, or (#443) when its scope segment was deleted (the definition is also disabled) or widened by a scrub. Any successful edit clears it."},"scopeSegmentName":{"type":"string","nullable":true,"description":"Scope segment display name, hydrated on reads (#443). Null when the definition is unscoped OR its scope segment has been deleted — a non-null scopeSegmentId with a null name is the dangling-scope state (the definition is disabled + degraded)."},"scopeSegmentKind":{"type":"string","enum":["company","contact","person"],"nullable":true,"description":"Scope segment home grain, hydrated on reads (#443). Null when unscoped/dangling."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}]},"AckdbScoreCapabilities":{"type":"object","description":"What this server's score validator accepts (#438): the contract a builder renders from instead of probing for support by saving throwaway definitions. Static, since it mirrors the validator's own constants, so it needs no DB round trip.","properties":{"grains":{"type":"array","items":{"type":"string","enum":["company","person"]},"description":"Grains a definition can be created on."},"scopes":{"type":"object","description":"Segment scoping (#443) — what scopeSegmentId accepts. Render the segment picker from this rather than assuming grain-for-grain: any segment grain may scope any score grain.","properties":{"segmentKinds":{"type":"array","items":{"type":"string","enum":["company","contact","person"]}},"derived":{"type":"boolean","description":"Whether a derived segment is a legal scope."}},"required":["segmentKinds","derived"]},"aggregates":{"type":"array","description":"Every aggregate reducer, with the grains and scoring modes each one allows. Render the aggregate picker from this: a scoring mode absent from `scoring` is a 400, not a fallback.","items":{"type":"object","properties":{"value":{"type":"string","enum":["count","sum","distinct_contacts","latest"]},"grains":{"type":"array","items":{"type":"string","enum":["company","person"]},"description":"Legal grains. distinct_contacts is company-only (the person grain has no contact rail)."},"scoring":{"type":"array","items":{"type":"string","enum":["bands","perUnitPoints","match"]},"description":"Scoring modes this reducer accepts. Only `latest` accepts `match`."},"requires":{"type":"string","enum":["sumField","valueField"],"description":"The extra field knob this reducer requires, if any."},"halfLife":{"type":"boolean","description":"False when halfLifeDays does not compose (distinct_contacts, latest), where it 400s."}},"required":["value","grains","scoring","halfLife"]}},"matchOps":{"type":"array","items":{"type":"string","enum":["eq","in","present"]},"description":"Match operators the validator accepts, on every matchable field kind."},"normalizations":{"type":"array","items":{"type":"string","enum":["absolute","percentile"]}},"percentilePopulations":{"type":"array","items":{"type":"string","enum":["all","active"]},"description":"Percentile ranking populations (#432). Rejected on absolute definitions."},"convergenceModes":{"type":"array","items":{"type":"string","enum":["sources","signals"]},"description":"Convergence list shapes, exactly one per criterion (#414 sources, #421 signals)."},"eventValueMatching":{"type":"string","enum":["exact"],"description":"How `match` values compare against event metadata: exact and case-sensitive, the way segment filters compare them (#380 canonicalizes trait values and deliberately left event fields exact). Seed value pickers from /fields/{key}/values rather than accepting free text, since a value differing only in case scores 0."},"limits":{"type":"object","description":"Every numeric bound the validator enforces. Enforce them client-side to avoid round-trip 400s.","properties":{"criteria":{"type":"integer","description":"Max criteria per definition.","example":50},"groups":{"type":"integer","description":"Max weighted groups per definition.","example":10},"matchEntries":{"type":"integer","description":"Max match entries per criterion (and values per entry)."},"bands":{"type":"integer","description":"Max bands per criterion."},"labels":{"type":"integer","description":"Max label bands per definition."},"points":{"type":"integer","description":"Absolute bound on match/band points (±).","example":100},"perUnitPoints":{"type":"integer","description":"Absolute bound on the per-unit slope (±)."},"cap":{"type":"integer","description":"Max perUnitPoints cap (0 < cap ≤ this)."},"windowDays":{"type":"integer","description":"Max trailing window on an event aggregate.","example":3650},"convergenceWindowDays":{"type":"integer","description":"Max window on a convergence criterion, tighter than windowDays.","example":365},"convergenceSignals":{"type":"integer","description":"Max signals in a signals-mode list.","example":20}},"required":["criteria","groups","matchEntries","bands","labels","points","perUnitPoints","cap","windowDays","convergenceWindowDays","convergenceSignals"]}},"required":["grains","aggregates","matchOps","normalizations","percentilePopulations","convergenceModes","eventValueMatching","scopes","limits"]},"AckdbScorePreview":{"type":"object","description":"The builder histogram: 20 five-point buckets over the 0–100 score axis.","properties":{"buckets":{"type":"array","items":{"type":"object","properties":{"from":{"type":"number"},"to":{"type":"number"},"count":{"type":"integer"}},"required":["from","to","count"]}},"labels":{"type":"array","items":{}},"total":{"type":"integer","description":"Rows in the histogram (Σ bucket counts) — equals population."},"population":{"type":"integer","description":"Rows the histogram ranks (#432): the active population for percentilePopulation: \"active\" definitions, else all rows of the grain (or, for a SCOPED definition, all rows of its scope)."},"outOfScope":{"type":"integer","description":"Rows of the grain the definition does not score AT ALL (#443) — outside its scope segment; 0 when unscoped. Distinct from `excluded`, which counts rows that ARE scored but not ranked. Together they let a builder say \"ranks 412 of 20,104 companies\"."},"excluded":{"type":"integer","description":"Rows outside the ranking population (#432) — 0 unless percentilePopulation: \"active\". The builder renders \"ranked among N active · M excluded\"."}},"required":["buckets","labels","total","population","excluded","outOfScope"]},"AckdbScoreExplain":{"type":"object","description":"Per-criterion score breakdown — identical math to the compiled sc__ column.","properties":{"scoreKey":{"type":"string"},"grain":{"type":"string","enum":["company","person"]},"score":{"type":"number","nullable":true,"example":82,"description":"The normalized 0–100 score. NULL when the row is outside a SCOPED definition's segment (#443) — the row is not scored, which is not the same statement as scoring 0, and its sc__ column reads NULL too."},"label":{"type":"string","nullable":true,"example":"Excellent"},"normalization":{"type":"string","enum":["absolute","percentile"]},"scope":{"type":"object","description":"SCOPED definitions only (#443): which segment the definition scores, and whether THIS row is in it. When inScope is false, score/label are null and the criteria below describe what the row WOULD contribute — render \"not scored — this model covers <segment>\", never a 0.","properties":{"segmentId":{"type":"string"},"segmentName":{"type":"string","nullable":true},"segmentKind":{"type":"string","enum":["company","contact","person"]},"inScope":{"type":"boolean"}},"required":["segmentId","segmentName","segmentKind","inScope"]},"groups":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"weight":{"type":"number"},"points":{"type":"number","description":"Σ of the group's criterion points (pre-weight)."}},"required":["key","weight","points"]}},"criteria":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"labelText":{"type":"string"},"group":{"type":"string"},"matched":{"type":"string","nullable":true,"description":"Match/band criteria: the row's raw value."},"aggValue":{"type":"number","nullable":true,"description":"Aggregate criteria: the raw aggregate. Null ONLY on an agg=latest criterion with no observed value (#424) — counting aggregates report 0."},"asOf":{"type":"string","format":"date-time","nullable":true,"description":"agg=latest criteria (#424): when the scored value was observed (the winning event's timestamp), so a UI can render \"1,240 as of Jul 12\". Null when nothing carried the field."},"points":{"type":"number"},"weighted":{"type":"number","description":"points × the group's weight."},"capped":{"type":"boolean","description":"perUnitPoints criteria: the ±cap clamp bit."},"firedSources":{"type":"array","items":{"type":"string"},"description":"convergence criteria: the listed sources that fired in the window (aggValue = count)."},"firedSignals":{"type":"array","items":{"type":"string"},"description":"signals-mode convergence criteria (#421): the listed signal refs that fired in the window, verbatim (aggValue = count)."}},"required":["field","labelText","group","points","weighted"]}},"populationExcluded":{"type":"boolean","description":"percentilePopulation: \"active\" definitions only (#432): present (true) when this row is outside the ranking population — no non-trait criterion fired, score is 0 and the row is in nobody's denominator. The FE renders \"no scored activity — not ranked\"."}},"required":["scoreKey","grain","score","normalization","groups","criteria"]},"AckdbScoreHistory":{"type":"object","description":"A subject's score series from the sweep's change-only `score.changed` events. `changes`: one point per emission (date = full ISO timestamp). `locf`: one point per UTC day (date = YYYY-MM-DD), last observation carried forward, seeded from before the range; days before the first-ever observation are omitted (never fabricated as zero).","properties":{"scoreKey":{"type":"string"},"grain":{"type":"string","enum":["company","person"]},"fill":{"type":"string","enum":["changes","locf"]},"points":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string"},"score":{"type":"number"},"label":{"type":"string","nullable":true}},"required":["date","score"]}}},"required":["scoreKey","grain","fill","points"]},"AckdbRecordDeletionCounts":{"type":"object","description":"Per-table row counts a delete removed. Every field is always present (0 when untouched).","properties":{"events":{"type":"integer","description":"Deleted. 0 for person/contact deletes — those DETACH instead."},"rawPayloads":{"type":"integer"},"files":{"type":"integer"},"contentChunks":{"type":"integer","description":"Derived: deleted, never archived; rebuilt by `npm run embed`."},"conversations":{"type":"integer","description":"Derived: rebuilt by `npm run conversations`."},"contacts":{"type":"integer"},"persons":{"type":"integer","description":"The record itself (person delete) or husks left behind."},"entities":{"type":"integer","description":"The record itself plus any merge tombstones pointing at it."},"identityLinks":{"type":"integer"},"contactIdentityLinks":{"type":"integer"},"personIdentityLinks":{"type":"integer"},"traits":{"type":"integer","description":"entity_traits + contact_traits + person_traits (attributes ride the same rails)."},"memberAssignments":{"type":"integer"},"mergeCandidates":{"type":"integer"},"segmentExits":{"type":"integer","description":"Memberships removed, each preceded by an `exit` in segment_changes."},"relationships":{"type":"integer","description":"Explicit company relationship episodes deleted."},"contactRelationships":{"type":"integer","description":"Contact hierarchy episodes deleted."}}},"AckdbRecordDeletionHeader":{"type":"object","description":"One archive entry: what was deleted, by whom, why, and whether its identifiers are still frozen.","properties":{"id":{"type":"string"},"kind":{"type":"string","enum":["company","person","contact"]},"recordId":{"type":"string"},"label":{"type":"string","nullable":true,"description":"Display name/domain/email captured AT DELETE TIME."},"reason":{"type":"string","nullable":true},"actor":{"type":"string","nullable":true},"status":{"type":"string","enum":["deleted","restored","purged"]},"tombstoneActive":{"type":"boolean","description":"Independent of `status`: a PURGED entry keeps its tombstone (the copy is destroyed, the record still must stay gone), and lifting the tombstone leaves the archive untouched."},"identifiers":{"$ref":"#/components/schemas/AckdbTombstoneIdentifiers"},"counts":{"$ref":"#/components/schemas/AckdbRecordDeletionCounts"},"detached":{"type":"object","properties":{"events":{"type":"integer"},"agentSessions":{"type":"integer"}}},"createdAt":{"type":"string","format":"date-time"},"restoredAt":{"type":"string","format":"date-time","nullable":true},"purgedAt":{"type":"string","format":"date-time","nullable":true},"archivedRows":{"type":"array","description":"Per-table archived row counts (GET /deletions/{id} only).","items":{"type":"object","properties":{"table":{"type":"string"},"rows":{"type":"integer"}}}}}},"AckdbTombstoneIdentifiers":{"type":"array","description":"The frozen `(type, value)` pairs, stored exactly as the resolvers store them (domains as ingested, emails lowercased, LinkedIn canonical) — the guard compares with plain equality.","items":{"type":"object","properties":{"type":{"type":"string"},"value":{"type":"string"}}}},"AckdbRecordDeletionResponse":{"type":"object","properties":{"data":{"type":"object","properties":{"dryRun":{"type":"boolean"},"deletionId":{"type":"string","nullable":true,"description":"Null on a dry run — no header is written."},"kind":{"type":"string","enum":["company","person","contact"]},"record":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string","nullable":true}}},"deleted":{"$ref":"#/components/schemas/AckdbRecordDeletionCounts"},"detached":{"type":"object","description":"Pointers nulled on rows that SURVIVE — events kept on company timelines, agent sessions.","properties":{"events":{"type":"integer"},"agentSessions":{"type":"integer"}}},"tombstone":{"type":"object","properties":{"active":{"type":"boolean"},"identifiers":{"$ref":"#/components/schemas/AckdbTombstoneIdentifiers"}}},"retained":{"type":"array","description":"What the delete deliberately left standing, and why (the #419 honesty trio).","items":{"type":"object","properties":{"kind":{"type":"string"},"count":{"type":"integer"},"reason":{"type":"string"}}}},"pendingRebuild":{"type":"array","description":"Always empty on a delete (a deleted company has nothing left to rebuild content for, and a person/contact delete does not touch content). A RESTORE is what populates it.","items":{"type":"object"}}}}}},"AckdbBulkRecordDeletionResponse":{"type":"object","properties":{"data":{"type":"object","properties":{"dryRun":{"type":"boolean"},"kind":{"type":"string","enum":["company","person","contact"]},"requested":{"type":"integer"},"deletedCount":{"type":"integer"},"failedCount":{"type":"integer"},"notFound":{"type":"array","items":{"type":"string"}},"results":{"type":"array","items":{"type":"object","properties":{"ref":{"type":"string"},"status":{"type":"string","enum":["deleted","notFound","failed"]},"result":{"type":"object","nullable":true},"error":{"type":"object","nullable":true,"properties":{"code":{"type":"string"},"message":{"type":"string"}}}}}}}}}},"AckdbDashboardKpiSpec":{"type":"object","description":"One number. `count` is the segment's row count and takes NO field; every other agg requires a numeric catalog FIELD KEY (numeric traits/attributes, `sc__` score columns, `evc__` per-event-type counts, `emf__` promoted event fields). Contact-aggregate (#214) and event-aggregate (#413) columns are inline column specs, not field keys, so they cannot be named here.","properties":{"agg":{"type":"string","enum":["count","sum","avg","min","max"]},"field":{"type":"string","description":"Required unless agg is `count`, forbidden when it is."}},"required":["agg"]},"AckdbDashboardBreakdownSpec":{"type":"object","description":"Group the segment's rows by one field and reduce each group to a number. A `date` groupBy is bucketed to the day; a NULL label is a real group (\"unset\"), never dropped.","properties":{"groupBy":{"type":"string","description":"A groupable catalog field: string, enum, boolean, date, score label, owner, identity-link column. Numeric fields are refused."},"agg":{"type":"string","enum":["count","sum","avg","min","max"]},"field":{"type":"string","description":"Required unless agg is `count`."},"topN":{"type":"integer","minimum":1,"maximum":50,"description":"Default 10."},"sort":{"type":"string","enum":["value_desc","value_asc","label_asc"],"description":"Default value_desc."},"othersBucket":{"type":"boolean","description":"Fold the remainder into one exact `others` entry. Default false."}},"required":["groupBy","agg"]},"AckdbDashboardTimeseriesSpec":{"type":"object","description":"One dense point per bucket. `bucket` x `windowDays` must yield at most 366 points (so a `day` bucket caps at 366 days), rejected at save/preview otherwise.","properties":{"metric":{"description":"Duck-typed union: an EVENT metric, a SCORE metric, or the MEMBERSHIP metric.","oneOf":[{"type":"object","description":"The segment members' own entity_events. `eventType` is the BARE name (resolved to the stored source-prefixed form, #355). `sum` needs a promoted numeric metadata `field`; `sum`/`distinct_contacts` need an aggregatable source on the grain (company today, #413). Zero-filled server-side.","properties":{"source":{"type":"string"},"eventType":{"type":"string","example":"invoice.paid"},"agg":{"type":"string","enum":["count","sum","distinct_contacts"]},"field":{"type":"string"}},"required":["source","eventType","agg"]},{"type":"object","description":"The AVG as-of score of segment members per month, read from the #411 `score.changed` stream. Month bucket only, at most 12 months, company or person grain, and the definition must exist and be ENABLED on that grain (else SCORE_NOT_FOUND). A month with no scored member is a null gap, never a fabricated 0.","properties":{"scoreKey":{"type":"string"}},"required":["scoreKey"]},{"type":"object","description":"Segment size over time: anchored at the exact live count and walked BACKWARDS through the S1 enter/exit log. Honest approximation - history starts at the segment's sync bootstrap and an enter+exit inside one diff interval cancels out. SYNC_UNAVAILABLE without the sync tables.","properties":{"membership":{"type":"boolean","enum":[true]}},"required":["membership"]}]},"bucket":{"type":"string","enum":["day","week","month"]},"windowDays":{"type":"integer","minimum":1,"maximum":730}},"required":["metric","bucket","windowDays"]},"AckdbDashboardTableSpec":{"type":"object","description":"The segment results rails with a columns/sort override (the pivot override contract). Omitted columns/sort inherit the segment's own; rows come back identical to GET /observe/segments/{id}/results.","properties":{"columns":{"type":"array","items":{"type":"object"},"description":"SegmentColumn[]."},"sort":{"type":"array","items":{"type":"object"},"description":"SegmentSort[] ({ field, dir })."},"limit":{"type":"integer","minimum":1,"maximum":100,"description":"Default 10."}}},"AckdbDashboardFeedSpec":{"type":"object","description":"Recent membership changes for the pinned segment, from the S1 change log. `reenter` is reported (and filtered) as `enter`.","properties":{"kinds":{"type":"array","items":{"type":"string","enum":["enter","exit"]},"description":"Default both."},"limit":{"type":"integer","minimum":1,"maximum":100,"description":"Default 20."},"windowDays":{"type":"integer","minimum":1,"maximum":730,"description":"Optional recency cut."}}},"AckdbDashboardSqlSpec":{"type":"object","description":"Escape hatch. A single read-only SELECT on the `ackdb_readonly` role (BEGIN READ ONLY + statement timeout + row/byte caps + extended wire protocol). Segment-less by design; never cached. No bind params in this build.","properties":{"sql":{"type":"string"},"columns":{"type":"array","description":"Declared by the author so the console renders without sniffing.","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"shape":{"type":"string","enum":["string","number","currency","date","boolean"]}},"required":["key","label","shape"]}},"maxRows":{"type":"integer","minimum":1,"maximum":5000,"description":"Default 1000."}},"required":["sql","columns"]},"AckdbDashboardPanel":{"type":"object","description":"One panel. `segmentId` is REQUIRED for every type except `sql`, which forbids it. The pinned segment must be a BASE segment: a derived segment stores only the filter on top of its base, so a panel compiling it alone would answer for the wrong population (refused at save/preview, fails closed at read).","properties":{"key":{"type":"string","pattern":"^[a-z0-9_-]{1,64}$","description":"Unique within the dashboard."},"title":{"type":"string"},"type":{"type":"string","enum":["kpi","breakdown","timeseries","table","feed","sql"]},"segmentId":{"type":"string"},"spec":{"description":"Per type.","oneOf":[{"$ref":"#/components/schemas/AckdbDashboardKpiSpec"},{"$ref":"#/components/schemas/AckdbDashboardBreakdownSpec"},{"$ref":"#/components/schemas/AckdbDashboardTimeseriesSpec"},{"$ref":"#/components/schemas/AckdbDashboardTableSpec"},{"$ref":"#/components/schemas/AckdbDashboardFeedSpec"},{"$ref":"#/components/schemas/AckdbDashboardSqlSpec"}]},"viz":{"$ref":"#/components/schemas/AckdbDashboardSqlViz","description":"For SQL panels, a validated line/bar/pie mapping; omit it for the existing table. Native panel types should omit this legacy hint. Max 20,000 serialized bytes."},"degraded":{"type":"boolean","readOnly":true,"description":"SERVER-OWNED, read-only. Set by the scrub paths (segment/source/score deletion); a degraded panel returns its degraded envelope and never runs. A request that CARRIES this field (or `degradedReason`) is rejected per panel: it would otherwise let a caller store a panel that skipped the segment-access gate, or un-disarm a panel a scrub disabled. Re-point a degraded panel at a live, accessible segment to revive it; that edit re-runs the full gate and clears the flag."},"degradedReason":{"type":"string","readOnly":true,"example":"segment deleted"}},"required":["key","title","type","spec"]},"AckdbDashboardArtifact":{"type":"object","description":"Escape-hatch HTML page plus the queries that feed it. Informational: it never defines what a panel means. HTML is capped at 5MB and scanned for credentials at save.","properties":{"html":{"type":"string"},"queries":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"sql":{"type":"string"}},"required":["key","sql"]}}},"required":["html","queries"]},"AckdbDashboard":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"ownerEmail":{"type":"string","nullable":true,"description":"null = service-created = org-visible."},"access":{"type":"string","enum":["owner","edit","view"],"description":"The caller's effective role."},"panels":{"type":"array","items":{"$ref":"#/components/schemas/AckdbDashboardPanel"}},"artifact":{"allOf":[{"$ref":"#/components/schemas/AckdbDashboardArtifact"}],"nullable":true},"layout":{"nullable":true,"description":"Console-owned grid arrangement, stored verbatim. Max 100,000 serialized bytes."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","name","panels","createdAt","updatedAt"]},"AckdbDashboardSummary":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"ownerEmail":{"type":"string","nullable":true},"panelCount":{"type":"integer"},"access":{"type":"string","enum":["owner","edit","view"]},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","name","panelCount","access","createdAt","updatedAt"]},"AckdbDashboardShare":{"type":"object","description":"One grant, identical in shape to a segment share (#284): the grantee is a MEMBER (internal_users).","properties":{"memberId":{"type":"string"},"role":{"type":"string","enum":["view","edit"]},"name":{"type":"string","nullable":true},"email":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"}},"required":["memberId","role"]},"AckdbDashboardCreateRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string","nullable":true},"panels":{"type":"array","items":{"$ref":"#/components/schemas/AckdbDashboardPanel"},"maxItems":24},"artifact":{"allOf":[{"$ref":"#/components/schemas/AckdbDashboardArtifact"}],"nullable":true},"layout":{"description":"Console-owned, stored verbatim."}},"required":["name"]},"AckdbDashboardUpdateRequest":{"type":"object","description":"A PATCH: an omitted key is left alone, an explicit `null` CLEARS description / artifact / layout.","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string","nullable":true},"panels":{"type":"array","items":{"$ref":"#/components/schemas/AckdbDashboardPanel"},"maxItems":24},"artifact":{"allOf":[{"$ref":"#/components/schemas/AckdbDashboardArtifact"}],"nullable":true},"layout":{"nullable":true}}},"AckdbPanelEnvelope":{"type":"object","description":"A panel's whole answer, always HTTP 200 for a data-shaped outcome. EXACTLY one arm: `data` (it ran), `degraded` (a scrub path disarmed it, or its segment is gone: no data, no 400), or `error` (timeout, read-only guard rejection, stale field - isolated to this panel).","oneOf":[{"type":"object","properties":{"data":{"oneOf":[{"$ref":"#/components/schemas/AckdbKpiData"},{"$ref":"#/components/schemas/AckdbBreakdownData"},{"$ref":"#/components/schemas/AckdbTimeseriesData"},{"$ref":"#/components/schemas/AckdbTableData"},{"$ref":"#/components/schemas/AckdbFeedData"},{"$ref":"#/components/schemas/AckdbSqlData"}]}},"required":["data"]},{"type":"object","properties":{"degraded":{"type":"boolean","enum":[true]},"reason":{"type":"string"}},"required":["degraded","reason"]},{"type":"object","properties":{"error":{"type":"object","description":"PANEL_COMPILE | SCORE_NOT_FOUND | SYNC_UNAVAILABLE | QUERY_ERROR | QUERY_TIMEOUT | ...","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}]},"AckdbKpiData":{"type":"object","description":"One number. An empty segment and a NULL aggregate both read null (\"nothing to average\", not 0).","properties":{"value":{"type":"number","nullable":true}},"required":["value"]},"AckdbBreakdownData":{"type":"object","properties":{"groups":{"type":"array","description":"At most topN, ordered per the spec. A null label is a real group (\"unset\"), never dropped.","items":{"type":"object","properties":{"label":{"type":"string","nullable":true},"value":{"type":"number","nullable":true}},"required":["label","value"]}},"total":{"type":"number","nullable":true,"description":"The AGGREGATE across ALL groups, pre-topN, in the same unit as `value` - NOT a row count (it is a row count only when agg is `count`). This is what a percent-of-total label needs."},"groupCount":{"type":"integer","description":"How many groups exist in total."},"others":{"type":"object","nullable":true,"description":"Present only with othersBucket:true AND more groups than were returned. Exact, derived from the two window functions.","properties":{"value":{"type":"number"},"groupCount":{"type":"integer"}}}},"required":["groups","total","groupCount","others"]},"AckdbTimeseriesData":{"type":"object","description":"Dense: one point per bucket, `t` is the bucket START (ISO-8601), ascending. Event and membership values are zero-filled numbers; a score month with no scored member is null.","properties":{"buckets":{"type":"array","items":{"type":"object","properties":{"t":{"type":"string","format":"date-time"},"value":{"type":"number","nullable":true}},"required":["t","value"]}}},"required":["buckets"]},"AckdbTableData":{"type":"object","description":"Rows verbatim from the grain's row compiler - byte-identical to GET /observe/segments/{id}/results.","properties":{"rows":{"type":"array","items":{"type":"object","additionalProperties":true}},"columns":{"type":"array","items":{"type":"string"},"description":"The projected keys in projection order (base columns included)."}},"required":["rows","columns"]},"AckdbFeedData":{"type":"object","properties":{"changes":{"type":"array","items":{"type":"object","properties":{"seq":{"type":"integer"},"id":{"type":"string","description":"Member id: entity_id / contact_id / person_id per the segment's grain."},"kind":{"type":"string","enum":["enter","exit"],"description":"A stored `reenter` is reported as `enter`."},"at":{"type":"string","format":"date-time"},"name":{"type":"string","nullable":true},"domain":{"type":"string","nullable":true,"description":"Company grain only."}},"required":["seq","id","kind","at","name"]}},"asOfSeq":{"type":"integer","description":"The segment sync seq this page was read at."},"historySince":{"type":"string","format":"date-time","nullable":true,"description":"The segment's sync bootstrap time. The feed structurally cannot show anything older - caption it so an empty feed does not read as \"nothing happened\"."}},"required":["changes","asOfSeq","historySince"]},"AckdbSqlData":{"type":"object","properties":{"columns":{"type":"array","items":{"type":"object"},"description":"Echoed from the spec."},"rows":{"type":"array","items":{"type":"object","additionalProperties":true}}},"required":["columns","rows"]},"AckdbDashboardFreshness":{"type":"object","properties":{"segments":{"type":"object","description":"Keyed by segment id. Every non-degraded panel's segment is present.","additionalProperties":{"type":"object","properties":{"seq":{"type":"integer","description":"GREATEST(max(seq), prunedThroughSeq); 0 when never diffed."},"lastChangeAt":{"type":"string","format":"date-time","nullable":true}},"required":["seq","lastChangeAt"]}},"checkedAt":{"type":"string","format":"date-time"}},"required":["segments","checkedAt"]},"AckdbCompanyRelationshipType":{"type":"object","properties":{"typeKey":{"type":"string","pattern":"^[a-z][a-z0-9_]{0,63}$"},"childToParentLabel":{"type":"string"},"parentToChildLabel":{"type":"string"},"description":{"type":"string","nullable":true},"origin":{"type":"string","enum":["standard","custom"]},"disabled":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["typeKey","childToParentLabel","parentToChildLabel","description","origin","disabled","createdAt","updatedAt"]},"AckdbCompanyReferenceInput":{"oneOf":[{"type":"object","properties":{"entityId":{"type":"string"}},"required":["entityId"],"additionalProperties":false},{"type":"object","properties":{"domain":{"type":"string"},"name":{"type":"string"}},"required":["domain"],"additionalProperties":false}]},"AckdbCompanyRelationshipProvenance":{"type":"object","properties":{"source":{"type":"string"},"workflow":{"type":"string"},"externalRecordId":{"type":"string"},"evidenceUrl":{"type":"string","format":"uri"},"observedAt":{"type":"string","format":"date-time"},"metadata":{"type":"object","additionalProperties":true}},"required":["source"],"additionalProperties":false},"AckdbCompanyRelationship":{"type":"object","description":"One explicit child-to-parent episode between distinct company entities. Multiple parents, multiple children, and cycles are valid.","properties":{"relationshipId":{"type":"string"},"class":{"type":"string","const":"company"},"childCompany":{"$ref":"#/components/schemas/AckdbCompanyGraphCompany"},"parentCompany":{"$ref":"#/components/schemas/AckdbCompanyGraphCompany"},"relationshipType":{"type":"string"},"childToParentLabel":{"type":"string"},"parentToChildLabel":{"type":"string"},"state":{"type":"string","enum":["active","ended","retracted"]},"effectiveFrom":{"type":"string","format":"date-time"},"effectiveTo":{"type":"string","format":"date-time","nullable":true},"retractedAt":{"type":"string","format":"date-time","nullable":true},"provenance":{"$ref":"#/components/schemas/AckdbCompanyRelationshipProvenance"},"createdBy":{"type":"string","nullable":true},"endedBy":{"type":"string","nullable":true},"endedReason":{"type":"string","nullable":true},"retractedBy":{"type":"string","nullable":true},"retractedReason":{"type":"string","nullable":true},"createdEventId":{"type":"string","nullable":true},"endedEventId":{"type":"string","nullable":true},"retractedEventId":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["relationshipId","class","childCompany","parentCompany","relationshipType","childToParentLabel","parentToChildLabel","state","effectiveFrom","effectiveTo","retractedAt","provenance","createdBy","endedBy","endedReason","retractedBy","retractedReason","createdEventId","endedEventId","retractedEventId","createdAt","updatedAt"]},"AckdbCompanyRelationshipSummary":{"type":"object","properties":{"requestedCompany":{"$ref":"#/components/schemas/AckdbCompanyGraphCompany"},"canonicalCompany":{"$ref":"#/components/schemas/AckdbCompanyGraphCompany"},"identityRelationships":{"type":"array","items":{"$ref":"#/components/schemas/AckdbCompanyIdentityRelationship"}},"mergedFromCount":{"type":"integer"},"parentCount":{"type":"integer"},"childCount":{"type":"integer"}},"required":["requestedCompany","canonicalCompany","identityRelationships","mergedFromCount","parentCount","childCount"]},"AckdbCompanyIdentityRelationship":{"type":"object","properties":{"class":{"type":"string","const":"identity"},"type":{"type":"string","const":"merged_into"},"company":{"$ref":"#/components/schemas/AckdbCompanyGraphCompany"},"canonicalCompany":{"$ref":"#/components/schemas/AckdbCompanyGraphCompany"}},"required":["class","type","company","canonicalCompany"]},"AckdbCompanyGraphResult":{"type":"object","properties":{"requestedCompany":{"$ref":"#/components/schemas/AckdbCompanyGraphCompany"},"canonicalCompany":{"$ref":"#/components/schemas/AckdbCompanyGraphCompany"},"identityRelationships":{"type":"array","items":{"$ref":"#/components/schemas/AckdbCompanyIdentityRelationship"}},"companies":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/AckdbCompanyGraphCompany"},{"type":"object","properties":{"depth":{"type":"integer"}},"required":["depth"]}]}},"companyRelationships":{"type":"array","items":{"$ref":"#/components/schemas/AckdbCompanyRelationship"}},"counts":{"type":"object","properties":{"companies":{"type":"integer"},"relationships":{"type":"integer"},"maxDepth":{"type":"integer"}},"required":["companies","relationships","maxDepth"]},"query":{"type":"object","properties":{"scope":{"type":"string","enum":["parents","children","connected"]},"relationshipTypes":{"type":"array","items":{"type":"string"},"nullable":true},"maxDepth":{"type":"integer","nullable":true},"asOf":{"type":"string","format":"date-time"},"includeRoot":{"type":"boolean"}},"required":["scope","relationshipTypes","maxDepth","asOf","includeRoot"]}},"required":["requestedCompany","canonicalCompany","identityRelationships","companies","companyRelationships","counts","query"]},"AckdbCompanyRelationshipPathResult":{"type":"object","properties":{"found":{"type":"boolean"},"requestedCompany":{"$ref":"#/components/schemas/AckdbCompanyGraphCompany"},"canonicalCompany":{"$ref":"#/components/schemas/AckdbCompanyGraphCompany"},"targetCompany":{"$ref":"#/components/schemas/AckdbCompanyGraphCompany"},"companies":{"type":"array","items":{"$ref":"#/components/schemas/AckdbCompanyGraphCompany"}},"companyRelationships":{"type":"array","items":{"$ref":"#/components/schemas/AckdbCompanyRelationship"}},"hopCount":{"type":"integer","nullable":true}},"required":["found","requestedCompany","canonicalCompany","targetCompany","companies","companyRelationships","hopCount"]},"AckdbCompanyGraphCompany":{"type":"object","properties":{"entityId":{"type":"string"},"name":{"type":"string","nullable":true},"domain":{"type":"string","nullable":true},"status":{"type":"string"},"graphOnly":{"type":"boolean"}},"required":["entityId","name","domain","status","graphOnly"]},"AckdbContactGraphContact":{"type":"object","description":"A contact affiliation projection used as an org-chart node.","properties":{"contactId":{"type":"string"},"entityId":{"type":"string"},"personId":{"type":"string"},"name":{"type":"string","nullable":true},"email":{"type":"string","nullable":true},"title":{"type":"string","nullable":true},"department":{"type":"string","nullable":true},"avatarUrl":{"type":"string","format":"uri","nullable":true},"isExternal":{"type":"boolean"},"status":{"type":"string","enum":["active","former","disabled"]},"startedAt":{"type":"string","format":"date-time","nullable":true},"endedAt":{"type":"string","format":"date-time","nullable":true}},"required":["contactId","entityId","personId","name","email","title","department","avatarUrl","isExternal","status","startedAt","endedAt"]},"AckdbContactGraphResult":{"type":"object","properties":{"entityId":{"type":"string"},"rootContact":{"allOf":[{"$ref":"#/components/schemas/AckdbContactGraphContact"}],"nullable":true},"contacts":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/AckdbContactGraphContact"},{"type":"object","properties":{"depth":{"type":"integer","minimum":0}},"required":["depth"]}]}},"contactRelationships":{"type":"array","items":{"$ref":"#/components/schemas/AckdbContactRelationship"}},"counts":{"type":"object","properties":{"contacts":{"type":"integer","minimum":0},"relationships":{"type":"integer","minimum":0},"maxDepth":{"type":"integer","minimum":0}},"required":["contacts","relationships","maxDepth"]},"query":{"type":"object","properties":{"scope":{"type":"string","enum":["managers","directReports","connected"]},"relationshipTypes":{"type":"array","items":{"type":"string"},"nullable":true},"maxDepth":{"type":"integer","nullable":true},"asOf":{"type":"string","format":"date-time"},"includeRoot":{"type":"boolean"}},"required":["scope","relationshipTypes","maxDepth","asOf","includeRoot"]}},"required":["entityId","rootContact","contacts","contactRelationships","counts","query"]},"AckdbContactPathResult":{"type":"object","properties":{"found":{"type":"boolean"},"entityId":{"type":"string"},"contacts":{"type":"array","items":{"$ref":"#/components/schemas/AckdbContactGraphContact"}},"contactRelationships":{"type":"array","items":{"$ref":"#/components/schemas/AckdbContactRelationship"}},"hopCount":{"type":"integer","minimum":0,"nullable":true}},"required":["found","entityId","contacts","contactRelationships","hopCount"]},"AckdbContactRelationship":{"type":"object","description":"One report-contact to manager-contact episode. Both endpoints are affiliations at entityId; matrix reporting is valid.","properties":{"relationshipId":{"type":"string"},"class":{"type":"string","const":"contact"},"entityId":{"type":"string"},"reportContact":{"$ref":"#/components/schemas/AckdbContactGraphContact"},"managerContact":{"$ref":"#/components/schemas/AckdbContactGraphContact"},"relationshipType":{"type":"string"},"reportToManagerLabel":{"type":"string"},"managerToReportLabel":{"type":"string"},"state":{"type":"string","enum":["active","ended","retracted"]},"effectiveFrom":{"type":"string","format":"date-time"},"effectiveTo":{"type":"string","format":"date-time","nullable":true},"retractedAt":{"type":"string","format":"date-time","nullable":true},"provenance":{"$ref":"#/components/schemas/AckdbContactRelationshipProvenance"},"createdBy":{"type":"string","nullable":true},"endedBy":{"type":"string","nullable":true},"endedReason":{"type":"string","nullable":true},"retractedBy":{"type":"string","nullable":true},"retractedReason":{"type":"string","nullable":true},"createdEventId":{"type":"string","nullable":true},"endedEventId":{"type":"string","nullable":true},"retractedEventId":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["relationshipId","class","entityId","reportContact","managerContact","relationshipType","reportToManagerLabel","managerToReportLabel","state","effectiveFrom","effectiveTo","retractedAt","provenance","createdBy","endedBy","endedReason","retractedBy","retractedReason","createdEventId","endedEventId","retractedEventId","createdAt","updatedAt"]},"AckdbContactRelationshipProvenance":{"type":"object","properties":{"source":{"type":"string"},"workflow":{"type":"string"},"externalRecordId":{"type":"string"},"evidenceUrl":{"type":"string","format":"uri"},"observedAt":{"type":"string","format":"date-time"},"metadata":{"type":"object","additionalProperties":true}},"required":["source"],"additionalProperties":false},"AckdbContactRelationshipSummary":{"type":"object","properties":{"contact":{"$ref":"#/components/schemas/AckdbContactGraphContact"},"managerCount":{"type":"integer","minimum":0},"directReportCount":{"type":"integer","minimum":0}},"required":["contact","managerCount","directReportCount"]},"AckdbContactRelationshipType":{"type":"object","properties":{"typeKey":{"type":"string","pattern":"^[a-z][a-z0-9_]{0,63}$"},"reportToManagerLabel":{"type":"string"},"managerToReportLabel":{"type":"string"},"description":{"type":"string","nullable":true},"origin":{"type":"string","enum":["standard","custom"]},"disabled":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["typeKey","reportToManagerLabel","managerToReportLabel","description","origin","disabled","createdAt","updatedAt"]},"AckdbDashboardSqlLineViz":{"type":"object","additionalProperties":false,"description":"Render ordered SQL rows as a line chart. `xKey` names a declared date/string/number column; `yKey` names a declared number/currency column. Row order is the query order.","properties":{"type":{"type":"string","enum":["line"]},"xKey":{"type":"string"},"yKey":{"type":"string"},"valueLabel":{"type":"string","description":"Optional tooltip noun; defaults to the y column label."}},"required":["type","xKey","yKey"]},"AckdbDashboardSqlCategoryViz":{"type":"object","additionalProperties":false,"description":"Render SQL rows as horizontal bars or a pie. `labelKey` names any declared column; `valueKey` must name a number/currency column. topN defaults to all returned rows; `othersBucket` combines the remainder.","properties":{"type":{"type":"string","enum":["bar","pie"]},"labelKey":{"type":"string"},"valueKey":{"type":"string"},"topN":{"type":"integer","minimum":1,"maximum":50},"othersBucket":{"type":"boolean","default":false}},"required":["type","labelKey","valueKey"]},"AckdbDashboardSqlViz":{"description":"A validated visualization mapping for a SQL panel. Omit `viz` to retain the declared-columns table.","oneOf":[{"$ref":"#/components/schemas/AckdbDashboardSqlLineViz"},{"$ref":"#/components/schemas/AckdbDashboardSqlCategoryViz"}]},"AckdbAgentWebSearchStatus":{"type":"object","description":"Effective web access for the agent. Trust `active`, not `enabled`: a deployment can have web access turned on while running a provider that cannot honor it, in which case /ask stays offline and `inactiveReason` says why. anthropic enforces maxUses + allow/block lists; openai enforces an allow-list only (no block-list, no cap); kimi/glm cannot search.","properties":{"enabled":{"type":"boolean","description":"Web access is turned on for this deployment."},"active":{"type":"boolean","description":"enabled AND the current provider supports it - the next /ask really can search."},"supported":{"type":"boolean","description":"The current provider can run a server-side web search."},"inactiveReason":{"type":"string","nullable":true,"description":"Why enabled web access is not active - unsupported provider, a custom openai baseUrl, or a domain list the provider cannot enforce (web access fails CLOSED rather than searching unrestricted). null when active or simply disabled."},"unhonoredSettings":{"type":"array","items":{"type":"string"},"description":"Budget-hint settings this provider silently cannot apply while search still runs (e.g. maxUses on openai). Boundary settings never appear here - they deactivate instead."},"source":{"type":"string","enum":["custom","default"],"description":"custom = stored via PUT /config/agent; default = AGENT_WEB_SEARCH env defaults."},"maxUses":{"type":"integer","nullable":true,"description":"Configured cap on searches per MODEL REQUEST (one /ask makes several). null = the default of 5 is sent. Enforced on anthropic only."},"allowedDomains":{"type":"array","items":{"type":"string"},"description":"Results limited to these hosts."},"blockedDomains":{"type":"array","items":{"type":"string"},"description":"Results never from these hosts."}}},"AckdbCompanyRelationshipMergeImpact":{"type":"object","properties":{"relationshipsRepointed":{"type":"integer","minimum":0},"relationshipDuplicatesConsolidated":{"type":"integer","minimum":0},"selfRelationshipsRetracted":{"type":"integer","minimum":0}},"required":["relationshipsRepointed","relationshipDuplicatesConsolidated","selfRelationshipsRetracted"]},"AckdbSegmentBootstrapPageResponse":{"allOf":[{"$ref":"#/components/schemas/AckdbMemberIdsResponse"},{"type":"object","properties":{"snapshotComplete":{"type":"boolean","description":"True once ackDB's durable snapshot is complete after this page commits."},"maxSeq":{"type":"integer","description":"Change-log head captured with this page."},"minRetainedSeq":{"type":"integer","description":"Earliest retained change sequence."}},"required":["snapshotComplete","maxSeq","minRetainedSeq"]}]}}},"paths":{"/api/v1/user/":{"get":{"operationId":"getUser","summary":"Get User","tags":["User"],"description":"Returns the authenticated caller's bootstrap payload: identity and org membership. Use List Workflows for accessible workflow metadata.","security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/User"}}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}},"description":"Maximum number of requests allowed in the current window"},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}},"description":"Number of requests remaining in the current window"},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}},"description":"Unix timestamp (seconds) when the current rate limit window resets"},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}},"description":"Number of seconds to wait before retrying"}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/user/knowledge":{"get":{"operationId":"getUserKnowledge","summary":"Get Knowledge","tags":["Organization"],"description":"Returns the caller's organization knowledge file — a single free-form text/markdown blob describing the org (who they are, what workflows they run, their goals). When no file has been written yet, returns `{ content: null, exists: false, updated_at: null }` rather than a 404. Write it with `PUT /api/v1/user/knowledge`.","security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/UserKnowledge"}}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}},"put":{"operationId":"putUserKnowledge","summary":"Set Knowledge","tags":["Organization"],"description":"Creates or replaces the caller's organization knowledge file — a single free-form text/markdown blob describing the org (who they are, what workflows they run, their goals). Idempotent full replacement: `content` becomes the entire file. Read it back with `GET /api/v1/user/knowledge`.\n\nMax size: 256 KB (UTF-8). To clear the file, send an empty string.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserKnowledgeBody"}}}},"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/UserKnowledge"}}}}}},"400":{"description":"Invalid request — `content` missing, not a string, or larger than 256 KB.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Invalid request — `content` missing, not a string, or larger than 256 KB."}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/org/members":{"post":{"operationId":"addMember","summary":"Add Member","tags":["Organization"],"description":"Invites a person to your organization. They receive an email invitation and hold no access until they accept, so the roster is unchanged until then.\n\n**Check `data.email_sent`.** The invitation is recorded whether or not the email goes out. When it is `false` the invitee was never told, and re-inviting is refused with `ALREADY_INVITED` — revoke it with Remove Member and invite again.\n\nRequires the `members:write` scope, which only an org admin's API key can carry.\n\nRefusals carry a `code`:\n\n| `code` | Status | Meaning |\n|--------|--------|---------|\n| `ALREADY_MEMBER` | 409 | That email is already on the roster |\n| `ALREADY_INVITED` | 409 | That email already has a pending invitation |\n| `MEMBER_LIMIT_REACHED` | 403 | Your plan's seat limit is full — pending invitations reserve a seat |\n| `NOT_ORG_LEAD` | 403 | The key's creator is no longer an org admin |\n","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrgMemberInviteBody"}}}},"security":[{"apiKey":[]}],"responses":{"201":{"description":"Created — the invitation is recorded and the invitee has been emailed","content":{"application/json":{"schema":{"type":"object","description":"Created — the invitation is recorded and the invitee has been emailed","properties":{"status":{"type":"integer"},"message":{"type":"string"},"data":{"$ref":"#/components/schemas/OrgMemberInvite"}}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden — not an org admin, or the plan's seat limit is full","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden — not an org admin, or the plan's seat limit is full"}}}},"409":{"description":"Conflict — already a member, or already invited","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Conflict — already a member, or already invited"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}},"delete":{"operationId":"removeMember","summary":"Remove Member","tags":["Organization"],"description":"Removes a person from your organization. Their API keys are revoked and their shared credentials are withdrawn from the org before the roster entry is dropped.\n\nIf the address has a pending invitation rather than a membership, the invitation is revoked — `data.removed` says which happened.\n\n**Check `data.warnings`.** Removal is never blocked, so anything of theirs the org was relying on is affected: workflows using a connection they owned start failing, active sources they owned stop importing, and workflows they still own keep running — scheduled runs charge the owner, so the org keeps paying for them. Each warning names the affected resources so you can reassign them. An empty array means none of those checks found anything, not that the member was inconsequential.\n\nRequires the `members:write` scope, which only an org admin's API key can carry. You cannot remove yourself.\n\nPercent-encode the address: a literal `+` in a query string decodes to a space, so `sam+team@acme.com` must be sent as `sam%2Bteam%40acme.com`.\n\nRefusals carry a `code`:\n\n| `code` | Status | Meaning |\n|--------|--------|---------|\n| `NOT_A_MEMBER` | 404 | No member or pending invitation for that address |\n| `CANNOT_REMOVE_SELF` | 403 | Another admin has to remove you |\n| `SSO_MEMBER_MANAGED_IN_IDP` | 403 | Managed by your SSO provider — remove them there |\n| `LAST_LEAD` / `LAST_MEMBER` | 409 | An org cannot be left with no admin or no members |\n","parameters":[{"schema":{"type":"string","minLength":1},"in":"query","name":"email","required":true,"description":"Email address of the member to remove, or of the pending invitation to revoke."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"message":{"type":"string"},"data":{"$ref":"#/components/schemas/OrgMemberRemoval"}}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden — not an org admin, removing yourself, or an SSO-managed member","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden — not an org admin, removing yourself, or an SSO-managed member"}}}},"404":{"description":"Not Found — no member or pending invitation for that address","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Not Found — no member or pending invitation for that address"}}}},"409":{"description":"Conflict — would leave the organization with no admin or no members","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Conflict — would leave the organization with no admin or no members"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/workflows/":{"post":{"operationId":"createWorkflow","summary":"Create Workflow","tags":["Workflows"],"description":"Creates a new empty workflow with a main sheet. Returns the `workflow_id` — this is also the main sheet's `sheet_id` (the two IDs are equal for main sheets). Pass `folder_id` from List Workflow Folders to create it in a folder; omit it for the top level.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWorkflowBody"}}}},"security":[{"apiKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/Workflow"}}},"example":{"status":201,"data":{"workflow_id":"531952c2-3d07-4be2-8c4b-733acba3187b","name":"Lead Enrichment Pipeline","created_at":"2026-04-20T10:30:00Z","updated_at":"2026-04-20T10:30:00Z","last_run_at":null,"folder_id":null,"folder_path":null}}}}},"400":{"description":"Missing required fields","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Missing required fields"}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Folder not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Folder not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}},"get":{"operationId":"listWorkflows","summary":"List Workflows","tags":["Workflows"],"description":"Returns all workflows the caller can access, sorted by most recently updated first. Each entry carries the ID, name, timestamps, and folder — no deeper state. Use Get Workflow Overview for sheet roster, settings, webhook URLs, and sharing on a single workflow.","security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Workflow"}}}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/workflows/folders":{"get":{"operationId":"listWorkflowFolders","summary":"List Workflow Folders","tags":["Workflows"],"description":"Returns the folders in the API key holder's workflow list, in display order. Folders are personal to each member. Each folder carries its parent and full path; List Workflows and Get Workflow Overview carry the matching `folder_id` on each workflow.","security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"type":"array","items":{"$ref":"#/components/schemas/WorkflowFolder"}}}},"example":{"status":200,"data":[{"folder_id":"6tB2xQ9mLr4KpZ1sVw0E","name":"Reporting","parent_folder_id":null,"path":["Reporting"]},{"folder_id":"Jq8nD3cYh7FvT5aRk2Xo","name":"Weekly","parent_folder_id":"6tB2xQ9mLr4KpZ1sVw0E","path":["Reporting","Weekly"]}]}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}},"post":{"operationId":"createWorkflowFolder","summary":"Create Workflow Folder","tags":["Workflows"],"description":"Creates a folder in the API key holder's workflow list, at the top level or inside `parent_folder_id`. Folders are personal to each member. Pass the returned `folder_id` to Create Workflow or Duplicate Workflow to place a workflow in it.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWorkflowFolderBody"}}}},"security":[{"apiKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/WorkflowFolder"}}},"example":{"status":201,"data":{"folder_id":"Jq8nD3cYh7FvT5aRk2Xo","name":"Weekly","parent_folder_id":"6tB2xQ9mLr4KpZ1sVw0E","path":["Reporting","Weekly"]}}}}},"400":{"description":"Empty name","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Empty name"}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Parent folder not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Parent folder not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/workflows/{workflow_id}":{"get":{"operationId":"getWorkflowOverview","summary":"Get Workflow Overview","tags":["Workflows"],"description":"Returns operational info for a single workflow — the bootstrap call after List Workflows.\n\nIncludes workflow metadata (`name`, timestamps), a per-sheet roster with settings (`auto_run`, cache), webhook URLs for pushing rows into each sheet, and sharing metadata.\n\n`data.sheets[0]` is always the main sheet; child sheets follow `sheets_config.sheetsOrder` when set, otherwise creation order.\n\n`is_owner` is `true` when the caller owns the workflow. `shared_users` lists who it is shared with, for the owner and org admins — the people who can share it (Share Workflow); other callers get `[]`.","parameters":[{"schema":{"type":"string"},"in":"path","name":"workflow_id","required":true,"description":"Workflow ID."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/WorkflowOverview"}}},"example":{"status":200,"data":{"workflow_id":"531952c2-3d07-4be2-8c4b-733acba3187b","name":"Lead Enrichment Pipeline","created_at":"2026-04-20T10:30:00Z","updated_at":"2026-04-20T10:30:00Z","last_run_at":null,"folder_id":"6tB2xQ9mLr4KpZ1sVw0E","folder_path":["Reporting","Weekly"],"is_owner":true,"shared_users":["colleague@example.com"],"sheets":[{"sheet_id":"531952c2-3d07-4be2-8c4b-733acba3187b","name":"Lead Enrichment Pipeline","is_main_sheet":true,"auto_run":false,"cache_enabled":false,"cache_since":null,"continue_on_fail":false,"webhook_url":"https://workers.floqer.com/v2/trigger/webhook?id=4f8a1c2e-7b3d-4e9a-a1c6-2d5f8e0b7c34.9f86d081884c7d659a2feaa0c55ad015"}]}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Workflow not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Workflow not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}},"delete":{"operationId":"deleteWorkflow","summary":"Delete Workflow","tags":["Workflows"],"description":"Permanently deletes a workflow and everything in it — all sheets, inputs, actions, rows, and run history. Cannot be undone.\n\nOnly the workflow owner can delete a workflow. Shared users and org members with workflow access cannot delete workflows they do not own.","parameters":[{"schema":{"type":"string"},"in":"path","name":"workflow_id","required":true,"description":"Workflow ID."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/WorkflowDeleted"}}},"example":{"status":200,"data":{"workflow_id":"531952c2-3d07-4be2-8c4b-733acba3187b","deleted":true}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Workflow not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Workflow not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/workflows/{workflow_id}/name":{"patch":{"operationId":"renameWorkflow","summary":"Rename Workflow","tags":["Workflows"],"description":"Sets the workflow's name. Single-field PATCH — the only body field is `name` (required, trimmed, non-empty).\n\n**This does not rename the main sheet's tab.** The workflow name and the main sheet's label are stored separately. To rename a sheet — main or child — use `PATCH /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/name` (Rename Sheet). Get Workflow Overview reports this name as `data.name`; `data.sheets[0].name` follows it only until the main sheet is given its own label.\n\n**Cache.** Renaming does not affect sheets, rows, cached action runs, or the action chain — only the name.\n\nAny caller with access to the workflow may rename it; unlike Delete Workflow this is not restricted to the owner.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenameWorkflowBody"}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"workflow_id","required":true,"description":"Workflow ID."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/WorkflowRenamed"}}},"example":{"status":200,"data":{"workflow_id":"531952c2-3d07-4be2-8c4b-733acba3187b","name":"Inbound lead enrichment"}}}}},"400":{"description":"Invalid request — `name` is empty after trimming","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Invalid request — `name` is empty after trimming"}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Workflow not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Workflow not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/workflows/{workflow_id}/duplicate":{"post":{"operationId":"duplicateWorkflow","summary":"Duplicate Workflow","tags":["Workflows"],"description":"Creates a new workflow in the caller's active organization by deep-copying the source workflow's configuration, action chains, and child sheets. Data rows, run history, and sharing permissions are not copied; the new workflow starts empty and is owned by the caller. Pass `folder_id` from List Workflow Folders to place the copy in a folder; omit it or pass `null` for the top level. If the source ID is a child sheet, the copy remains attached to the same parent workflow; a non-null `folder_id` is refused there.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DuplicateWorkflowBody"}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"workflow_id","required":true,"description":"ID of the workflow to duplicate."}],"security":[{"apiKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/WorkflowDuplicated"}}},"example":{"status":201,"data":{"workflow_id":"531952c2-3d07-4be2-8c4b-733acba3187b","name":"Lead Enrichment Pipeline Copy"}}}}},"400":{"description":"Invalid workflow ID, empty name, or a non-null `folder_id` on a sheet","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Invalid workflow ID, empty name, or a non-null `folder_id` on a sheet"}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Workflow or folder not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Workflow or folder not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/workflows/{workflow_id}/shares":{"patch":{"operationId":"shareWorkflow","summary":"Share Workflow","tags":["Workflows"],"description":"Shares the workflow with members of your organization and stops sharing it with others, in one call. Sharing a workflow gives edit access, and it appears in their workflow list.\n\nOnly the workflow's owner and org admins can do this. People in `add` who already have it, and the owner, are reported in `data.skipped`; people in `remove` who did not have it are ignored. Read the current list from Get Workflow Overview (`shared_users`).\n\nA refusal changes nothing; its `code` says why, and `emails` lists any refused addresses.","requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"minProperties":1,"properties":{"add":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"string","format":"email"},"description":"Email addresses to share with. They must be current members of your organization.","example":["sam@acme.com"]},"remove":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"string","minLength":1},"description":"Email addresses to stop sharing with. Former members can be removed too.","example":["kim@acme.com"]}}}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"workflow_id","required":true,"description":"Workflow ID."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"message":{"type":"string"},"data":{"type":"object","properties":{"workflow_id":{"type":"string"},"owner_email":{"type":"string"},"shared_users":{"type":"array","items":{"type":"string"},"description":"Everyone the workflow is shared with, after this call."},"added":{"type":"array","items":{"type":"string"},"description":"Newly shared with."},"skipped":{"type":"array","items":{"type":"string"},"description":"The owner, and people who already had it."},"removed":{"type":"array","items":{"type":"string"},"description":"No longer shared with."}}}}}}}},"400":{"description":"Bad Request — invalid body, or a refused change (`code` + `emails`)","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"errors":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"message":{"type":"string"}},"required":["field","message"]}},"code":{"type":"string"},"emails":{"type":"array","items":{"type":"string"}}},"required":["error","message"],"description":"Bad Request — invalid body, or a refused change (`code` + `emails`)"}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction). SHARE_FORBIDDEN: you can open the workflow but are not its owner or an org admin.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction). SHARE_FORBIDDEN: you can open the workflow but are not its owner or an org admin."}}}},"404":{"description":"Workflow not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Workflow not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/workflows/{workflow_id}/sheets":{"post":{"operationId":"createSheet","summary":"Create Sheet","tags":["Sheets"],"description":"Adds a new sheet to the workflow identified by `workflow_id`. Returns the full sheet object. Pass the returned `sheet_id` as the `{sheet_id}` path parameter on every sheet-scoped endpoint (Add Inputs, Add Action, Add Data, Run Sheet, etc.).\n\n**Routing data into the new sheet.** Sheets receive rows via the `send_to_sheet` action on another sheet in the same workflow. On the source sheet, add `send_to_sheet`, set `target_sheet_id` to the `sheet_id` returned here, and map source fields to the new sheet's input columns. To expand a `structured_array` (one row per array item), set `expand_from` on the `send_to_sheet` action. Full configuration: `/docs/action-detail/send_to_sheet.txt`.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSheetBody"}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"workflow_id","required":true,"description":"Workflow ID."}],"security":[{"apiKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/Sheet"}}},"example":{"status":201,"data":{"sheet_id":"e5f6a7b8-c9d0-1e2f-3a4b-5c6d7e8f9a0b","workflow_id":"531952c2-3d07-4be2-8c4b-733acba3187b","name":"Employees","is_main_sheet":false,"auto_run":true,"cache_enabled":true,"cache_since":null,"continue_on_fail":false}}}}},"400":{"description":"Invalid request body","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Invalid request body"}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Workflow not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Workflow not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/workflows/{workflow_id}/sheets/{sheet_id}/name":{"patch":{"operationId":"renameSheet","summary":"Rename Sheet","tags":["Sheets"],"description":"Sets a sheet's name. Single-field PATCH — the only body field is `name` (required, trimmed, non-empty).\n\n**Renaming the main sheet does NOT rename the workflow.** Pass `sheet_id === workflow_id` to rename the main sheet's tab; the workflow's own name is left untouched, and Get Workflow Overview keeps returning it as `data.name` while `data.sheets[0].name` changes to the new label. To rename the workflow itself, use `PATCH /api/v1/workflows/{workflow_id}/name` (Rename Workflow).\n\n**Cache.** Renaming does not affect row data, cached action runs, or the action chain — only the sheet's name. The workflow's `updated_at` timestamp is bumped.\n\n**Sheet ID convention:** sheet IDs come from Get Workflow Overview (`data.sheets[].sheet_id`) or from Create Sheet. The main sheet's ID equals the workflow ID.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenameSheetBody"}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"workflow_id","required":true,"description":"Parent workflow ID."},{"schema":{"type":"string"},"in":"path","name":"sheet_id","required":true,"description":"Sheet ID to rename. Pass the workflow's own ID to rename the main sheet's tab (the workflow name is not changed)."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/SheetRenamed"}}},"example":{"status":200,"data":{"workflow_id":"531952c2-3d07-4be2-8c4b-733acba3187b","sheet_id":"e5f6a7b8-c9d0-1e2f-3a4b-5c6d7e8f9a0b","name":"Qualified employees"}}}}},"400":{"description":"Invalid request — `name` is empty after trimming","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Invalid request — `name` is empty after trimming"}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Workflow or sheet not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Workflow or sheet not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/workflows/{workflow_id}/sheets/{sheet_id}":{"patch":{"operationId":"updateSheet","summary":"Update Sheet","tags":["Sheets"],"description":"Updates a sheet's settings. Send only the fields to change — at least one of `auto_run`, `cache_enabled`, `cache_since`; omitted fields are left untouched.\n\n`auto_run: true` (the default on every sheet) runs the action chain on each row that lands — from Add Data Rows, a synced source (initial backfill and live rows alike), or `send_to_sheet`. `auto_run: false` loads rows without running them; run them later with Run Rows (`POST .../run` with `row_ids`). Applies to rows that arrive after the change; rows already on the sheet are untouched.\n\n`cache_enabled` and `cache_since` are the same settings Create Sheet accepts: whether identical-input rows reuse cached outputs, and the earliest date (`YYYY-MM-DD`, UTC) a cached run may be from to count as fresh. `cache_since: null` clears the cutoff.\n\nWorks on the main sheet: pass `sheet_id === workflow_id`.\n\nReturns the full updated sheet.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSheetBody"}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"workflow_id","required":true,"description":"Parent workflow ID."},{"schema":{"type":"string"},"in":"path","name":"sheet_id","required":true,"description":"Sheet ID to update. Pass the workflow's own ID for the main sheet."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/Sheet"}}},"example":{"status":200,"data":{"sheet_id":"531952c2-3d07-4be2-8c4b-733acba3187b","workflow_id":"531952c2-3d07-4be2-8c4b-733acba3187b","name":"Inbound leads","is_main_sheet":true,"auto_run":false,"cache_enabled":true,"cache_since":"2026-03-17","continue_on_fail":false}}}}},"400":{"description":"Invalid request — no settable field present, a non-boolean toggle, or a malformed `cache_since`","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Invalid request — no settable field present, a non-boolean toggle, or a malformed `cache_since`"}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Workflow or sheet not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Workflow or sheet not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}},"delete":{"operationId":"deleteSheet","summary":"Delete Sheet","tags":["Sheets"],"description":"Permanently deletes a child sheet from the workflow. Archives the sheet and its action chain; row data and run history on the sheet are no longer reachable. Cannot be undone.\n\nOnly the sheet owner can delete a child sheet. Shared users and org members with workflow access cannot delete sheets they do not own.\n\nThe workflow's main sheet cannot be deleted (`sheet_id` must not equal `workflow_id`). To remove the entire workflow including its main sheet, use Delete Workflow.\n\n**Sheet ID convention:** pass the child sheet's UUID as `sheet_id` — not the parent workflow ID.","parameters":[{"schema":{"type":"string"},"in":"path","name":"workflow_id","required":true,"description":"Parent workflow ID."},{"schema":{"type":"string"},"in":"path","name":"sheet_id","required":true,"description":"Child sheet ID to delete. Must not equal `workflow_id` (the main sheet)."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/SheetDeleted"}}},"example":{"status":200,"data":{"workflow_id":"531952c2-3d07-4be2-8c4b-733acba3187b","sheet_id":"e5f6a7b8-c9d0-1e2f-3a4b-5c6d7e8f9a0b","deleted":true}}}}},"400":{"description":"Invalid request — e.g. attempting to delete the main sheet (`sheet_id === workflow_id`).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Invalid request — e.g. attempting to delete the main sheet (`sheet_id === workflow_id`)."}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Workflow or sheet not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Workflow or sheet not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/workflows/{workflow_id}/sheets/{sheet_id}/inputs":{"get":{"operationId":"listInputs","summary":"List Inputs","tags":["Inputs"],"description":"Returns all input fields currently configured for a workflow sheet. Use this to check existing inputs before adding or updating.\n\n**Sheet ID convention:** to operate on the workflow's main sheet, pass the workflow's own ID as `sheet_id` — the main sheet's ID equals the workflow ID. For additional sheets under the workflow, pass the child sheet's ID.\n\n**Referencing inputs in downstream actions:** each input's `reference` field is a ready-to-use variable token (`{{input.<name>}}`) that can be dropped into action `inputString` configurations to pull the input's value at run time.","parameters":[{"schema":{"type":"string"},"in":"path","name":"workflow_id","required":true,"description":"Workflow ID."},{"schema":{"type":"string"},"in":"path","name":"sheet_id","required":true,"description":"Sheet ID. To operate on the workflow's main sheet, pass the workflow's ID as `sheet_id` — the main sheet's ID equals the workflow ID."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Input"},"description":"Configured input fields, in the order they are defined on the sheet."}}},"example":{"status":200,"data":[{"name":"linkedin_url","type":"url","description":"LinkedIn company profile URL","reference":"{{input.linkedin_url}}"},{"name":"email","type":"email","description":"","reference":"{{input.email}}"}]}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Workflow or sheet not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Workflow or sheet not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}},"post":{"operationId":"addInputs","summary":"Add Inputs","tags":["Inputs"],"description":"Adds input fields to the workflow. Existing inputs are not affected. Returns all inputs (existing + new). Reference input fields in actions with `{{input.field_name}}` syntax.\n\n**Request body:** a JSON array — no wrapper object needed.\n\n**Valid `type` values:** `string`, `url`, `email`, `number`.\n\n**Sheet ID convention:** to operate on the workflow's main sheet, pass the workflow's own ID as `sheet_id` — the main sheet's ID equals the workflow ID. For additional sheets under the workflow, pass the child sheet's ID.","requestBody":{"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/InputCreate"},"description":"Input fields to add to the sheet. Each item: `name`, `type`, and optional `description`."},"example":[{"name":"linkedin_url","type":"url","description":"LinkedIn company profile URL"},{"name":"email","type":"email"},{"name":"company_name","type":"string","description":"Target company name"}]}},"description":"Input fields to add to the sheet. Each item: `name`, `type`, and optional `description`."},"parameters":[{"schema":{"type":"string"},"in":"path","name":"workflow_id","required":true,"description":"Workflow ID."},{"schema":{"type":"string"},"in":"path","name":"sheet_id","required":true,"description":"Sheet ID. To operate on the workflow's main sheet, pass the workflow's ID as `sheet_id` — the main sheet's ID equals the workflow ID."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Input"},"description":"Every input on the sheet after the add — existing inputs followed by the newly added ones, each with its `reference` token."}}},"example":{"status":200,"data":[{"name":"linkedin_url","type":"url","description":"LinkedIn company profile URL","reference":"{{input.linkedin_url}}"},{"name":"email","type":"email","description":"","reference":"{{input.email}}"},{"name":"company_name","type":"string","description":"Target company name","reference":"{{input.company_name}}"}]}}}},"400":{"description":"Invalid request — e.g. duplicate field name or unsupported type","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Invalid request — e.g. duplicate field name or unsupported type"}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Workflow or sheet not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Workflow or sheet not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/workflows/{workflow_id}/sheets/{sheet_id}/inputs/{field_name}":{"delete":{"operationId":"deleteInput","summary":"Delete Input","tags":["Inputs"],"description":"Removes a single input field from the workflow. Any action references using `{{input.<field_name>}}` for this field will break. Check your action configurations before deleting.\n\n**Sheet ID convention:** to operate on the workflow's main sheet, pass the workflow's own ID as `sheet_id` — the main sheet's ID equals the workflow ID. For additional sheets under the workflow, pass the child sheet's ID.\n\n**Response:** the array of remaining inputs on the sheet after the delete, in the order they are defined.","parameters":[{"schema":{"type":"string"},"in":"path","name":"workflow_id","required":true,"description":"Workflow ID."},{"schema":{"type":"string"},"in":"path","name":"sheet_id","required":true,"description":"Sheet ID. To operate on the workflow's main sheet, pass the workflow's ID as `sheet_id` — the main sheet's ID equals the workflow ID."},{"schema":{"type":"string"},"examples":{"email":{"value":"email"},"linkedin_url":{"value":"linkedin_url"}},"in":"path","name":"field_name","required":true,"description":"The `name` of the input field (e.g. `email`). Use List Inputs to see all field names."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Input"},"description":"Remaining input fields on the sheet after the delete, in the order they are defined."}}},"example":{"status":200,"data":[{"name":"email","type":"email","description":"","reference":"{{input.email}}"}]}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Workflow, sheet, or field not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Workflow, sheet, or field not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/add":{"post":{"operationId":"addActionToWorkflow","summary":"Add Action to Workflow","tags":["Actions"],"description":"Adds an action to the workflow. Returns the `action_instance_id`, input fields to configure, and output fields with reference strings.\n\nAppends to the end of the chain by default. Use `after` to insert after a specific action — **`after` is an `action_instance_id`, NOT an `action_id`**.\n\nPass `name` to set a custom display name for the new instance. Omit to use the action's default name.After adding, use Configure Action to wire input variables.\n\nackDB actions support ingest, company/person/contact/member lookup, company/person/contact update/upsert, and company/contact relationships. The legacy `ackdb_merge_record` remains a company-merge alias. New `ackdb_ingest` actions expose boolean `accepted` first; company creation remains the separate `entity_created` output. Accepted includes retries, queued requests and retained unresolved content, and excludes dropped or failed items. It does not prove a new event was created or queued processing finished.\n\n**Sheet ID convention:** to operate on the workflow's main sheet, pass the workflow's own ID as `sheet_id` — the main sheet's ID equals the workflow ID. For additional sheets under the workflow, pass the child sheet's ID.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddActionBody"}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"workflow_id","required":true,"description":"Workflow ID."},{"schema":{"type":"string"},"in":"path","name":"sheet_id","required":true,"description":"Sheet ID. To operate on the workflow's main sheet, pass the workflow's ID as `sheet_id` — the main sheet's ID equals the workflow ID."}],"security":[{"apiKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"warnings":{"type":"array","items":{"$ref":"#/components/schemas/ConfigureActionWarning"},"description":"Non-fatal issues with the request: unknown top-level fields, plus anything the optional inline `inputs` raised (unknown fields, unresolved or downstream references, and environment-variable problems). Same codes as Configure Action. Absent when the request was applied cleanly."},"data":{"$ref":"#/components/schemas/ActionInstance"}}},"example":{"status":201,"data":{"action_instance_id":"scrape_person_linkedin_profile_1","action_id":"scrape_person_linkedin_profile","display_name":"Scrape Person LinkedIn Profile","inputs":[{"name":"linkedin_url","type":"url","required":true,"description":"LinkedIn profile URL of the person"}],"outputs":[{"name":"full_name","type":"string","reference":"{{scrape_person_linkedin_profile_1.full_name}}"},{"name":"headline","type":"string","reference":"{{scrape_person_linkedin_profile_1.headline}}"},{"name":"current_company","type":"string","reference":"{{scrape_person_linkedin_profile_1.current_company}}"},{"name":"current_company_domain","type":"url","reference":"{{scrape_person_linkedin_profile_1.current_company_domain}}"},{"name":"experiences","type":"raw_array","reference":"{{scrape_person_linkedin_profile_1.experiences}}","description":"Work history as JSON. Each item: title, company, start_date, end_date, summary, url, company_domain, company_identifier"},{"name":"education","type":"raw_array","reference":"{{scrape_person_linkedin_profile_1.education}}","description":"Education history as JSON. Each item: school, degree, field, start_date, end_date"}]}}}}},"400":{"description":"Invalid request — `after` names an action_instance_id on this sheet that is connected to the chain, so the new action runs. For `call_shortcut`, `shortcut` must name a shortcut not backed by this workflow (`code: \"SHORTCUT_SELF_CALL\"`).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"},"warnings":{"type":"array","items":{"$ref":"#/components/schemas/ConfigureActionWarning"},"description":"Unknown top-level body keys, reported alongside the error so a misplaced key is still visible when the request fails for another reason. Absent when every top-level key was read."}},"description":"Invalid request — `after` names an action_instance_id on this sheet that is connected to the chain, so the new action runs. For `call_shortcut`, `shortcut` must name a shortcut not backed by this workflow (`code: \"SHORTCUT_SELF_CALL\"`)."}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Workflow, sheet, or action template not found — or, for `call_shortcut`, a `shortcut` you cannot see (`code: \"SHORTCUT_NOT_FOUND\"`)","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Workflow, sheet, or action template not found — or, for `call_shortcut`, a `shortcut` you cannot see (`code: \"SHORTCUT_NOT_FOUND\"`)"}}}},"409":{"description":"For `call_shortcut`: the `shortcut` has no published version (`code: \"SHORTCUT_NOT_PUBLISHED\"`). Republish it in the Floqer app first.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"For `call_shortcut`: the `shortcut` has no published version (`code: \"SHORTCUT_NOT_PUBLISHED\"`). Republish it in the Floqer app first."}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/{action_instance_id}":{"patch":{"operationId":"configureAction","summary":"Configure Action","tags":["Actions"],"description":"Configures an action instance. Send only the fields you want to set — unset fields keep their current value.\n\n**Body fields:**\n- `inputs` — map of field name → value. String values can be literals, public reference tokens (`{{input.<name>}}` / `{{<action_instance_id>.<name>}}`), or a mix. String-array values are used for waterfall-provider fields (ordered provider IDs).\n- Environment variables are referenced the same way, as `{{env.<key>}}` — see **List Environment Variables** for the available keys. Two cases come back as warnings on a 200: `env_variable_unavailable` (the variable is `workflow` scoped with no value on this workflow — saved, but rows error until one is set) and `env_not_supported_in_conditions` (an environment variable in `run_if` or a Filter action's `path_conditions`; they are substituted after conditions are evaluated, so that condition is skipped).\n- `run_if` — optional gate. AND/OR condition groups: `{conditions: [{conditions: [{variable, operator, values?, combinator?}], combinator?}]}` — the same shape as the filter action's `path_conditions` (a single condition is one group with one leaf). Pass `null` to clear an existing condition.\n- `continue_workflow_if_action_fails` — when `true`, downstream actions still run for a row even if this action fails; `null` clears the action's own setting so it follows the sheet's `continue_on_fail`.\n- `note` — optional free-text note to save on the action. Same upsert as Save Action Note (`POST /:action_instance_id/notes`).\n- `output_types` — map of output name → declared type, for outputs whose type is a per-action choice (today Formula's `result`): `string` (default), `number`, `boolean`, `date`, `currency`, `email`, `url`, `imageUrl`, `json`. The type decides which `run_if` / `filter` operators apply; a condition takes it when the condition is saved, so set it before adding conditions on that output.\n\nFor action-specific configuration guidance (prompts, waterfall provider IDs, model selection), see the action detail file at `/docs/action-detail/{action_id}.txt`.\n\n**Jev: AI Classify (`ai_classify`):** set `inputs.input` to row text/references and `inputs.questions` to a JSON array (or its JSON string). Each question has a stable UUID `id`, `name`, `type` (`yes_no`, `pick_one`, `score`) and `question`; pick-one requires 2–255 unique labeled `options`, score requires 2–10 ordered `levels`. The server validates questions (400 on invalid input) and derives answer/confidence columns plus a level column for scores, preserving column IDs by question ID across edits. All derived names must be unique after snake-casing. Removing or retyping referenced columns returns a `broken_downstream_reference` warning naming affected actions. Get Action/Get Outputs returns the columns and their reference tokens. Use Get Action Field Options on `questions` for templates and plain-language drafts.\n\n**ackDB:** discover Source, Events, settable fields and enum choices through Get Action Field Options. Lookup type spells company `entity`; Update Scope spells it `company`. Configure field lists as `[{name, value}]`, preserving JSON false/0. Person-only ingest ownership comes from the registered event definition, not a request toggle.\n\nOrdinary configure/publish reconciles a missing `ackdb_ingest.accepted` output into live action metadata, preserving existing output UUIDs, names, order and references. A still-old live schema can also acquire it when a new worker completes with a real Accepted boolean. Read-only Get Action/Get Outputs do not upgrade ingest schemas or historical rows. An old worker lacking Accepted omits that output. Immutable shortcut versions remain unchanged: republish a new version for the expanded schema. Conflicting preexisting Accepted UUIDs fail the save instead of silently rewriting references.\n\n**Cache invalidation.** Any change to this action's configuration — including whitespace-only edits — invalidates the cache for every row already processed through it. That action, and every action downstream, re-runs and re-bills on the next execution.\n\n**Sheet ID convention:** to operate on the workflow's main sheet, pass the workflow's own ID as `sheet_id` — the main sheet's ID equals the workflow ID. For additional sheets under the workflow, pass the child sheet's ID.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfigureActionBody"}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"workflow_id","required":true,"description":"Workflow ID."},{"schema":{"type":"string"},"in":"path","name":"sheet_id","required":true,"description":"Sheet ID. To operate on the workflow's main sheet, pass the workflow's ID as `sheet_id` — the main sheet's ID equals the workflow ID."},{"schema":{"type":"string"},"examples":{"enrich_company_linkedin_profile_1":{"value":"enrich_company_linkedin_profile_1"},"llm_models_1":{"value":"llm_models_1"}},"in":"path","name":"action_instance_id","required":true,"description":"The action instance ID (e.g. `enrich_company_linkedin_profile_1`). Returned by Add Action to Workflow."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"warnings":{"type":"array","items":{"$ref":"#/components/schemas/ConfigureActionWarning"},"description":"Non-fatal issues detected while applying the request (e.g. unknown fields, unresolved references). Absent or empty when the request was applied cleanly."},"data":{"type":"object","properties":{"action_instance_id":{"type":"string","description":"Echoes the action instance that was configured.","example":"llm_models_1"}},"required":["action_instance_id"]}}},"example":{"status":200,"warnings":[{"field":"model","code":"unknown_field","message":"Field 'model' is not recognized by this action and was ignored."},{"field":"prompt","code":"unresolved_reference","message":"Reference {{input.linkdin_url}} does not match any input field. Did you mean {{input.linkedin_url}}?"}],"data":{"action_instance_id":"llm_models_1"}}}}},"400":{"description":"Invalid request body — including, for `call_shortcut`, a `shortcut` backed by this workflow (`code: \"SHORTCUT_SELF_CALL\"`)","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"},"warnings":{"type":"array","items":{"$ref":"#/components/schemas/ConfigureActionWarning"},"description":"Unknown top-level body keys, reported alongside the error so a misplaced key is still visible when the request fails for another reason. Absent when every top-level key was read."}},"description":"Invalid request body — including, for `call_shortcut`, a `shortcut` backed by this workflow (`code: \"SHORTCUT_SELF_CALL\"`)"}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Workflow, sheet, or action instance not found — or, for `call_shortcut`, a `shortcut` you cannot see (`code: \"SHORTCUT_NOT_FOUND\"`)","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Workflow, sheet, or action instance not found — or, for `call_shortcut`, a `shortcut` you cannot see (`code: \"SHORTCUT_NOT_FOUND\"`)"}}}},"409":{"description":"Another request changed this action's type, Attio object, push target or called shortcut while this one was being applied (nothing was written; retry the request), or the shortcut set on a `call_shortcut` action has no published version (`code: \"SHORTCUT_NOT_PUBLISHED\"`).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Another request changed this action's type, Attio object, push target or called shortcut while this one was being applied (nothing was written; retry the request), or the shortcut set on a `call_shortcut` action has no published version (`code: \"SHORTCUT_NOT_PUBLISHED\"`)."}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}},"delete":{"operationId":"deleteAction","summary":"Delete Action","tags":["Actions"],"description":"Removes an action from the sheet's chain by relinking its previous and next neighbours so the chain skips it.\n\n**Returns the affected inputs.** Any action whose configuration references one of the deleted action's outputs is listed in `dependent_actions[]`, with `inputs` carrying only the fields that actually referenced the deleted action. Values are translated to public reference form so the broken refs are easy to spot. Unrelated fields on the same dependent action are not included. The caller is expected to fix the listed fields via Configure Action.\n\nCannot delete the input action — pass any other `action_instance_id`.\n\n**Sheet ID convention:** to operate on the workflow's main sheet, pass the workflow's own ID as `sheet_id` — the main sheet's ID equals the workflow ID. For additional sheets under the workflow, pass the child sheet's ID.","parameters":[{"schema":{"type":"string"},"in":"path","name":"workflow_id","required":true,"description":"Workflow ID."},{"schema":{"type":"string"},"in":"path","name":"sheet_id","required":true,"description":"Sheet ID. To operate on the workflow's main sheet, pass the workflow's ID as `sheet_id` — the main sheet's ID equals the workflow ID."},{"schema":{"type":"string"},"examples":{"enrich_company_linkedin_profile_1":{"value":"enrich_company_linkedin_profile_1"},"llm_models_1":{"value":"llm_models_1"}},"in":"path","name":"action_instance_id","required":true,"description":"The action instance ID to delete (e.g. `enrich_company_linkedin_profile_1`). Returned by Add Action to Workflow, or found in the Get Action Graph response."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"type":"object","properties":{"deleted":{"type":"boolean","description":"Always `true` on a 200 response — the action was removed from the chain."},"action_instance_id":{"type":"string","description":"Echoes the action instance that was deleted."},"dependent_actions":{"type":"array","description":"Actions whose configuration referenced one of the deleted action's outputs. Empty when nothing depended on it. Each entry carries only the affected fields — keys are the dependent action's input field names (snake-cased), values are the stored `inputString` translated to public reference form so the broken refs are obvious. The caller is expected to re-wire these via Configure Action.","items":{"type":"object","properties":{"action_instance_id":{"type":"string","description":"The dependent action's instance ID."},"inputs":{"type":"object","description":"Map of `<snake_field_name>` → input value (string). Only fields that referenced the deleted action are included; unrelated fields on the same action are omitted.","additionalProperties":{"type":"string"}}},"required":["action_instance_id","inputs"]}}},"required":["deleted","action_instance_id","dependent_actions"]}}},"example":{"status":200,"data":{"deleted":true,"action_instance_id":"enrich_company_linkedin_profile_1","dependent_actions":[{"action_instance_id":"llm_models_1","inputs":{"prompt":"Write a cold email to {{enrich_company_linkedin_profile_1.company_name}} about {{enrich_company_linkedin_profile_1.industry}}."}}]}}}}},"400":{"description":"Invalid request. The input action (`id1`) is permanent. An action with several paths (a splitter, if-else or path filter) is deleted on the canvas, as is an action whose removal would send two paths of one action to the same place.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Invalid request. The input action (`id1`) is permanent. An action with several paths (a splitter, if-else or path filter) is deleted on the canvas, as is an action whose removal would send two paths of one action to the same place."}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Workflow, sheet, or action instance not found. Reserved IDs (`input`, `graph`) also return 404.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Workflow, sheet, or action instance not found. Reserved IDs (`input`, `graph`) also return 404."}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}},"get":{"operationId":"getAction","summary":"Get Action","tags":["Actions"],"description":"Returns a single action from a workflow. The response `data` is the same per-node shape emitted by Get Action Graph — use this endpoint when you only need one action and don't want to fetch the entire graph.\n\n**Round-tripping with Configure Action:** the `inputs` map in this response matches the body Configure Action accepts. GET the current action, modify `inputs`, then PATCH the same URL (Configure Action) to save.\n\n**Notes.** If a note has been saved on this action via Save Action Note (or Configure Action's `note` field), it's surfaced as `data.note`. Absent when no note has been saved.\n\n**Validation warnings on read.** The response includes a `warnings` array whenever the action's current configuration has issues (unresolved references, downstream references, required fields missing). Agents inspecting a workflow they didn't build — or revisiting one after changes upstream — can spot broken config without having to re-PATCH. Same warning shape as Configure Action's response, so one parsing path covers both.\n\n**Reserved `action_instance_id` values:** `\"input\"` (the input pseudo-node — use List Inputs or `data[0]` of Get Action Graph) and `\"graph\"` (the graph endpoint path — use Get Action Graph). Both return 404 here.\n\n**Sheet ID convention:** to operate on the workflow's main sheet, pass the workflow's own ID as `sheet_id` — the main sheet's ID equals the workflow ID. For additional sheets under the workflow, pass the child sheet's ID.","parameters":[{"schema":{"type":"string"},"in":"path","name":"workflow_id","required":true,"description":"Workflow ID."},{"schema":{"type":"string"},"in":"path","name":"sheet_id","required":true,"description":"Sheet ID. To operate on the workflow's main sheet, pass the workflow's ID as `sheet_id` — the main sheet's ID equals the workflow ID."},{"schema":{"type":"string"},"examples":{"enrich_company_linkedin_profile_1":{"value":"enrich_company_linkedin_profile_1"},"llm_models_1":{"value":"llm_models_1"}},"in":"path","name":"action_instance_id","required":true,"description":"The action instance ID (e.g. `enrich_company_linkedin_profile_1`). Returned by Add Action to Workflow, or found in the Get Action Graph response. Reserved values: `\"input\"` (input pseudo-node) and `\"graph\"` (the graph endpoint path) — both return 404 here."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"warnings":{"type":"array","items":{"$ref":"#/components/schemas/ConfigureActionWarning"},"description":"Non-fatal configuration issues detected on the current action (unresolved references, missing required fields, etc.). Absent or empty when the configuration is clean."},"data":{"$ref":"#/components/schemas/ActionGraphNode"}}},"example":{"status":200,"warnings":[{"field":"linkedin_url","code":"unresolved_reference","message":"Reference {{input.linkdin_url}} does not match any input field. Did you mean {{input.linkedin_url}}?"}],"data":{"action_instance_id":"enrich_company_linkedin_profile_1","action_id":"enrich_company_linkedin_profile","display_name":"Scrape Company LinkedIn Profile","inputs":{"linkedin_url":"{{input.linkdin_url}}"},"outputs":[{"name":"company_name","type":"string","reference":"{{enrich_company_linkedin_profile_1.company_name}}"},{"name":"industry","type":"string","reference":"{{enrich_company_linkedin_profile_1.industry}}"},{"name":"employee_count","type":"number","reference":"{{enrich_company_linkedin_profile_1.employee_count}}"}],"run_if":{"conditions":[{"conditions":[{"variable":"{{input.country}}","operator":"is","values":["US","CA"]}]}],"continue_workflow_if_run_condition_not_met":false},"continue_workflow_if_action_fails":false,"next":["llm_models_1"]}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Workflow, sheet, or action instance not found. Also returned for reserved IDs `\"input\"` and `\"graph\"`.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Workflow, sheet, or action instance not found. Also returned for reserved IDs `\"input\"` and `\"graph\"`."}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/{action_instance_id}/name":{"patch":{"operationId":"renameAction","summary":"Rename Action","tags":["Actions"],"description":"Sets the display name of an action instance. Single-field PATCH — the only body field is `name`.\n\n**Cache.** Renaming does NOT invalidate the row-level cache for this action — display name doesn't change what the action does. Only the workflow's `updated_at` timestamp is bumped.\n\n**Sheet ID convention:** to operate on the workflow's main sheet, pass the workflow's own ID as `sheet_id` — the main sheet's ID equals the workflow ID. For additional sheets under the workflow, pass the child sheet's ID.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenameActionBody"}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"workflow_id","required":true,"description":"Workflow ID."},{"schema":{"type":"string"},"in":"path","name":"sheet_id","required":true,"description":"Sheet ID. To operate on the workflow's main sheet, pass the workflow's ID as `sheet_id` — the main sheet's ID equals the workflow ID."},{"schema":{"type":"string"},"examples":{"enrich_company_linkedin_profile_1":{"value":"enrich_company_linkedin_profile_1"},"llm_models_1":{"value":"llm_models_1"}},"in":"path","name":"action_instance_id","required":true,"description":"The action instance ID (e.g. `enrich_company_linkedin_profile_1`). Returned by Add Action to Workflow."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"type":"object","properties":{"action_instance_id":{"type":"string"},"display_name":{"type":"string"}},"required":["action_instance_id","display_name"]}}},"example":{"status":200,"data":{"action_instance_id":"enrich_company_linkedin_profile_1","display_name":"Enrich primary contact"}}}}},"400":{"description":"Invalid request — e.g. `name` is empty after trimming","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Invalid request — e.g. `name` is empty after trimming"}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Workflow, sheet, or action instance not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Workflow, sheet, or action instance not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/{action_instance_id}/move":{"patch":{"operationId":"moveAction","summary":"Move Action","tags":["Actions"],"description":"Re-orders an action within the sheet's chain. Updates chain pointers on the moved action, its old neighbours, and its new neighbours; does not touch the action's configured inputs or outputs.\n\n**Positioning.** `after` is required. Pass an existing `action_instance_id` to place the action immediately after that instance. `after` cannot equal the action being moved.\n\nCannot move the input action — it's the chain's anchor.\n\n**Reference safety.** Move does NOT rewrite the action's configured inputs. If the new position pushes the action ahead of one of its upstream references — or pushes a downstream consumer ahead of this action — those references will fail at run time. Fix broken refs afterwards via Configure Action.\n\n**Sheet ID convention:** to operate on the workflow's main sheet, pass the workflow's own ID as `sheet_id` — the main sheet's ID equals the workflow ID.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MoveActionBody"}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"workflow_id","required":true,"description":"Workflow ID."},{"schema":{"type":"string"},"in":"path","name":"sheet_id","required":true,"description":"Sheet ID. To operate on the workflow's main sheet, pass the workflow's ID as `sheet_id` — the main sheet's ID equals the workflow ID."},{"schema":{"type":"string"},"examples":{"enrich_company_linkedin_profile_1":{"value":"enrich_company_linkedin_profile_1"},"llm_models_1":{"value":"llm_models_1"}},"in":"path","name":"action_instance_id","required":true,"description":"The action instance ID to move (e.g. `enrich_company_linkedin_profile_1`). Returned by Add Action to Workflow, or found in the Get Action Graph response."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"type":"object","properties":{"moved":{"type":"boolean","description":"Always `true` on a 200 response — the action has been re-ordered."},"action_instance_id":{"type":"string","description":"Echoes the action instance that was moved."},"after":{"type":"string","description":"The `action_instance_id` the moved action now sits immediately after. `\"input\"` indicates the moved action now sits at the start of the chain (right after the synthetic input node)."}},"required":["moved","action_instance_id","after"]}}},"example":{"status":200,"data":{"moved":true,"action_instance_id":"enrich_company_linkedin_profile_1","after":"scrape_person_linkedin_profile_1"}}}}},"400":{"description":"Invalid request. The input action (`id1`) stays first. `after` names a different action on this sheet that is connected to the chain. An action with several paths (a splitter, if-else or path filter) is moved on the canvas, as is an action whose move would send two paths of one action to the same place.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Invalid request. The input action (`id1`) stays first. `after` names a different action on this sheet that is connected to the chain. An action with several paths (a splitter, if-else or path filter) is moved on the canvas, as is an action whose move would send two paths of one action to the same place."}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Workflow, sheet, or action instance not found. Reserved IDs (`input`, `graph`) also return 404.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Workflow, sheet, or action instance not found. Reserved IDs (`input`, `graph`) also return 404."}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/{action_instance_id}/waterfall/{field_name}":{"patch":{"operationId":"reorderWaterfall","summary":"Reorder Waterfall Providers","description":"Re-orders the provider list on a waterfall (`stepDownSearch`) input field. Pass the snake-cased `field_name` from Add Action's `inputs[]` and a `providers` array listing every currently configured provider **by display name** exactly once, in the desired execution order. Names are matched case-insensitively; read the current set from Get Action's `inputs[<field>]`.\n\n**Item shape.** Items may be bare names or objects shaped `{ name }`. Get Action returns providers running on your own API key as `{ name, use_own_key: true }`, so its array can be reordered and sent back unchanged — no need to flatten it first.\n\nThis endpoint only reorders — it does not add or remove providers, and it never changes key state (`use_own_key` in the body is ignored, with a `warnings[]` entry when it contradicts what is stored). To change which providers are enabled, or which run on your own key, use Configure Action.\n\n**Cache.** Same as Configure Action — reordering invalidates the row-level cache for this action and every downstream action.\n\n**Sheet ID convention:** to operate on the workflow's main sheet, pass the workflow's own ID as `sheet_id` — the main sheet's ID equals the workflow ID.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReorderWaterfallBody"}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"workflow_id","required":true,"description":"Workflow ID."},{"schema":{"type":"string"},"in":"path","name":"sheet_id","required":true,"description":"Sheet ID. To operate on the workflow's main sheet, pass the workflow's ID as `sheet_id` — the main sheet's ID equals the workflow ID."},{"schema":{"type":"string"},"example":"person_work_email_waterfall_1","in":"path","name":"action_instance_id","required":true,"description":"The action instance whose waterfall field to reorder (e.g. `person_work_email_waterfall_1`). Returned by Add Action."},{"schema":{"type":"string"},"example":"email_providers","in":"path","name":"field_name","required":true,"description":"Snake-cased waterfall field name from the action's `inputs[]` (e.g. `email_providers`). Must be a `stepDownSearch` field on the action template."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"warnings":{"type":"array","items":{"$ref":"#/components/schemas/ConfigureActionWarning"},"description":"Non-fatal issues detected while applying the reorder — `ambiguous_provider` (a token matched one provider's name and another's legacy id; resolved by name) or `own_key_ignored` (`use_own_key` in the body disagreed with stored state and was ignored). Absent when the request applied cleanly."},"data":{"type":"object","properties":{"action_instance_id":{"type":"string"},"field_name":{"type":"string"},"providers":{"type":"array","items":{"type":"string"},"description":"The provider display names in their new execution order."}},"required":["action_instance_id","field_name","providers"]}}},"example":{"status":200,"data":{"action_instance_id":"person_work_email_waterfall_1","field_name":"email_providers","providers":["Hunter","Prospeo","Lightning"]}}}}},"400":{"description":"Invalid request — e.g. field is not a waterfall field, providers list is empty, contains duplicates, or does not match the currently configured provider set.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Invalid request — e.g. field is not a waterfall field, providers list is empty, contains duplicates, or does not match the currently configured provider set."}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Workflow, sheet, action instance, or field not found.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Workflow, sheet, action instance, or field not found."}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/{action_instance_id}/notes":{"post":{"operationId":"saveActionNote","summary":"Save Action Note","tags":["Actions"],"description":"Saves a free-text note on an action instance. One note per action — calling again for the same action overwrites the existing note. Notes are surfaced back on the action via Get Action / Get Action Graph (read path lands later).\n\n**Sheet ID convention:** to operate on the workflow's main sheet, pass the workflow's own ID as `sheet_id` — the main sheet's ID equals the workflow ID. For additional sheets under the workflow, pass the child sheet's ID.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SaveNoteBody"}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"workflow_id","required":true,"description":"Workflow ID."},{"schema":{"type":"string"},"in":"path","name":"sheet_id","required":true,"description":"Sheet ID. To operate on the workflow's main sheet, pass the workflow's ID as `sheet_id` — the main sheet's ID equals the workflow ID."},{"schema":{"type":"string"},"examples":{"enrich_company_linkedin_profile_1":{"value":"enrich_company_linkedin_profile_1"},"llm_models_1":{"value":"llm_models_1"}},"in":"path","name":"action_instance_id","required":true,"description":"The action instance to attach the note to (e.g. `enrich_company_linkedin_profile_1`). Returned by Add Action."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/ActionNote"}}},"example":{"status":200,"data":{"action_instance_id":"enrich_company_linkedin_profile_1","sheet_id":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","note":"Skip this until Phase 2 — vendor still negotiating contract terms."}}}}},"400":{"description":"Invalid request — e.g. `note` is empty after trimming","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Invalid request — e.g. `note` is empty after trimming"}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Workflow, sheet, or action instance not found / reserved","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Workflow, sheet, or action instance not found / reserved"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/{action_instance_id}/options/{field_name}":{"post":{"operationId":"getOptions","summary":"Get Action Field Options","tags":["Actions"],"description":"Fetches the dropdown / dynamic option values for one of an action's input fields. Use it when configuring an action whose field expects a value from a connected integration (Salesforce objects, Instantly campaigns, HubSpot properties, etc.).\n\nPass the action's `action_instance_id` and the snake-cased `field_name` (both come back in Add Action's response) — the server dispatches to the matching resolver internally. There is no `reference_id` / `valuesId` for callers to track.\n\nCascading resolvers (e.g. Salesforce: pick object → pick external ID field) take their parent selection via `context`. Each cascading resolver declares its required context keys; calling without them returns a 400 with the list of missing keys so callers can fetch the parent first.\n\n**Jev: AI Classify (`ai_classify`), field `questions`:** send `{}` to list templates, including required input guidance in `extras.needs` (`{label, hint}` entries). Send `{context: {description: \"Classify the industry and rate fit\", input: \"Optional example row or column references\"}}` to draft questions from plain words using the same generator as the app. Description and input are each limited to 20,000 characters. Drafting requires the `workflows:write` scope and is limited to 30 drafts per organization per hour (429 when exceeded). Each option's `value` is a JSON string ready to save as `inputs.questions`; `extras.questions` contains the equivalent array with server-assigned UUIDs. Drafting does not save or run the action. Invalid draft context returns 400; provider failures, refusals, truncated JSON and invalid generated questions return 502. Keep question IDs when editing saved questions to preserve output column IDs.\n\n**ackDB:** `ackdb_ingest:source` lists enabled source tokens. `ackdb_entity_lookup:events` uses `context.lookup_type` (`entity` for company, `person`, `contact`, or `member`), falling back to the saved literal Lookup type only when context omits it. Missing, unresolved, or invalid lookup types return 400; they never silently select company. Person options include person-only events; company options exclude them. Event coverage is data-gated, so a newly registered type may not appear until data exists. Values are `source:event_type`. Contact/member intentionally return no event options.\n\n`ackdb_entity_lookup` also exposes `company_attribute`, `person_attribute`, and `contact_attribute`: scalar text, number, boolean and date attributes owned by that record type. These catalogs need no context. Configure the returned `value` with `exact_attribute_value`, `exact_person_attribute_value`, or `exact_contact_attribute_value`, respectively; send the exact value as a string. Use the matching `lookup_type` (`entity`, `person`, or `contact`) and leave the other search fields blank. Text equality is case-sensitive; multiple matches fail without choosing a record. An explicit record ID takes priority. Contact attribute lookup supports active affiliations at active companies only and rejects `include_former_contacts:true`. Existing name, identity and former-contact field searches are unchanged.\n\n`ackdb_update_record` exposes `company_fields_to_set`, `person_fields_to_set`, and `contact_fields_to_set`, including registered attributes, core fields, supported identity keys, and company/contact owners. Optional `context.scope` must match the selected field catalog. Lookup type, Scope and boolean switches also expose static options; boolean option strings `true`/`false` can be configured as JSON booleans. `ackdb_company_relationship` exposes `relationship_target`, `operation`, and `relationship_type`; explicit context overrides its saved target. Relationship targets remain case-insensitive; legacy missing/empty target defaults to company. Company operations are create/end/retract/merge; contact operations are contact_create/contact_end/contact_retract. The legacy `ackdb_merge_record` action has no dropdown fields.\n\nackDB catalogs use the workflow connection's stored baseUrl/apiKey, like action execution. A missing connection returns 424; upstream failures return 502 instead of silently substituting another grain or a partial field catalog.\n\nReturns options as `{ value, label, extras? }`. `value` is what to send back via Configure Action; `extras` carries per-integration metadata when relevant.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetOptionsBody"}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"workflow_id","required":true,"description":"Workflow ID."},{"schema":{"type":"string"},"in":"path","name":"sheet_id","required":true,"description":"Sheet ID. To operate on the workflow's main sheet, pass the workflow's ID as `sheet_id` — the main sheet's ID equals the workflow ID."},{"schema":{"type":"string"},"examples":{"salesforce_upsert_record_1":{"value":"salesforce_upsert_record_1"},"hubspot_create_object_1":{"value":"hubspot_create_object_1"}},"in":"path","name":"action_instance_id","required":true,"description":"The action instance ID whose field needs options. Returned by Add Action, Get Action, or Get Action Graph."},{"schema":{"type":"string"},"examples":{"salesforce_object":{"value":"salesforce_object"},"hubspot_object_properties":{"value":"hubspot_object_properties"},"select_slack_channel":{"value":"select_slack_channel"}},"in":"path","name":"field_name","required":true,"description":"Snake-cased input field name from the action's `inputs[]` (matches what Add Action returns and what Configure Action accepts)."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/GetOptionsData"}}},"example":{"status":200,"data":{"options":[{"value":"Account","label":"Account"},{"value":"Contact","label":"Contact"},{"value":"Lead","label":"Lead"}]}}}}},"400":{"description":"Invalid request — e.g. the field is not dynamic on this action, the field name doesn't exist on the template, or required `context` keys are missing.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Invalid request — e.g. the field is not dynamic on this action, the field name doesn't exist on the template, or required `context` keys are missing."}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Workflow / sheet / action instance not found, or the action_instance_id is reserved (`input`, `graph`).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Workflow / sheet / action instance not found, or the action_instance_id is reserved (`input`, `graph`)."}}}},"424":{"description":"The user is not connected to the underlying integration (e.g. no Salesforce OAuth). Surface as a 'connect first' CTA.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"The user is not connected to the underlying integration (e.g. no Salesforce OAuth). Surface as a 'connect first' CTA."}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}},"502":{"description":"Upstream integration call failed (provider down, expired token, etc.).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Upstream integration call failed (provider down, expired token, etc.)."}}}}}}},"/api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/{action_instance_id}/outputs":{"get":{"operationId":"getActionOutputSchema","summary":"Get Action Output Schema","tags":["Actions"],"description":"Returns the resolved output schema for an action: every field the action produces, each with a ready-to-paste `reference` token for downstream wiring.\n\n**When to use this** instead of reading `outputs` from Get Action / Get Action Graph: user-defined outputs can gain fields and samples after a row runs. A fresh `http_api_call` always exposes exactly `response` (the complete returned body) and `metadata`; append a JSON path to `response` to reference a nested value. Target 4xx/5xx statuses are completed results, with the body in `response` and status in `metadata`; only transport, timeout, the 25 MB response-size limit, or Floqer processing failures fail the cell. To materialize a nested value as a column, create a separate downstream `formula` action. After Save and run first cell, inspect actual results using Get Row or List Rows; runs do not write ping. No separate Test Request or Preview is required. Rootless legacy HTTP actions retain their configured projection fields and may derive flat fields from a stored ping without adopting root defaults. Derived fields are persisted only when the key also has `workflows:write`; read-only keys never write. Static actions (Salesforce / HubSpot / etc.) work too, with the same payload as Get Action's `outputs`.\n\n**`structured_array` outputs** (a list of structured rows) carry their per-column schema under `columns[]`, each with its own `structured_array_reference` token shaped `{{<action_instance_id>.<list>.<column>}}`. Drop those tokens verbatim into a downstream action's Configure Action body. The runtime is the source of truth for reference syntax.\n\nFor `ackdb_ingest`, this GET reports the stored schema without reconciling old 19-output configurations. Configure/publish or a future boolean-bearing live completion adds Accepted while preserving old UUIDs. Existing row/cell outputs are historical stored values: they are never backfilled, even when the current schema includes Accepted. The frontend may derive a legacy display from recorded Results; the public API does not invent that field. Frozen shortcut versions require republishing a new version.\n\n**Reserved `action_instance_id` values:** `\"input\"` (use List Inputs) and `\"graph\"` (the graph endpoint path) return 404.","parameters":[{"schema":{"type":"string"},"in":"path","name":"workflow_id","required":true,"description":"Workflow ID."},{"schema":{"type":"string"},"in":"path","name":"sheet_id","required":true,"description":"Sheet ID. To operate on the workflow's main sheet, pass the workflow's ID as `sheet_id` — the main sheet's ID equals the workflow ID."},{"schema":{"type":"string"},"examples":{"raw_to_structured_array_1":{"value":"raw_to_structured_array_1"},"http_api_call_1":{"value":"http_api_call_1"}},"in":"path","name":"action_instance_id","required":true,"description":"The action instance whose output schema to return. Returned by Add Action, Get Action, or Get Action Graph."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"type":"object","properties":{"action_instance_id":{"type":"string","description":"Echo of the path parameter, for response-level self-description."},"outputs":{"type":"array","items":{"$ref":"#/components/schemas/ActionOutput"},"description":"Fields this action produces, each with a ready-to-paste `reference` token for downstream wiring. Empty array for actions that produce no fields (e.g. `filter`)."}},"required":["action_instance_id","outputs"]}}},"example":{"status":200,"data":{"action_instance_id":"raw_to_structured_array_1","outputs":[{"name":"list","type":"structured_array","reference":"{{raw_to_structured_array_1.list}}","description":"Structured array built from the input array.","columns":[{"name":"first_name","type":"string","structured_array_reference":"{{raw_to_structured_array_1.list.first_name}}"},{"name":"email","type":"email","structured_array_reference":"{{raw_to_structured_array_1.list.email}}"}]}]}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Workflow / sheet / action instance not found, or the action_instance_id is reserved (`\"input\"`, `\"graph\"`).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Workflow / sheet / action instance not found, or the action_instance_id is reserved (`\"input\"`, `\"graph\"`)."}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/graph":{"get":{"operationId":"getActionGraph","summary":"Get Action Graph","tags":["Actions"],"description":"Returns every node in a workflow as a flat array, ordered topologically from the input node onward.\n\n**Reading the response:**\n- `data[0]` is always the input node (`action_id: \"input\"`). It represents the workflow's input columns and has no `inputs` of its own — its `outputs` list every input field with ready-to-paste `{{input.<field_name>}}` references. If no inputs have been defined yet, `outputs: []`.\n- `data[1..]` are the actions, in topological execution order from the entry action onward. If no actions have been added yet, `data` contains only the input node with `next: []`.\n- Walk the chain by following each node's `next` array. `next: []` is terminal.\n- Multiple IDs in `next` represent a split path — today only one downstream ID is populated, but the shape accommodates future branching actions without changing.\n- Resolving variable references: `{{X.field}}` → find the node where `action_instance_id === \"X\"` → look in its `outputs`. This rule works uniformly for `{{input.linkedin_url}}` and for action outputs like `{{enrich_company_linkedin_profile_1.company_name}}`.\n\n**What each action node carries:**\n- `inputs` — the variable wiring you set via Configure Action. Unconfigured fields are simply absent from the map.\n- `outputs` — the fields this action produces, each with a ready-to-paste `reference` string. Absent on actions with no outputs (e.g. `filter`).\n- `continue_workflow_if_action_fails` — present only when the action has its own setting. Absent means the action follows the sheet's `continue_on_fail` (see Get Workflow Overview; default off). `true` lets the chain skip past this action's failure for a row.\n- `run_if` — present only when a condition is configured via Configure Action. Always returned in the nested `{conditions: [groups]}` form (a single condition is one group with one leaf), the same shape Configure Action accepts.\n- `note` — present only when a note has been saved on the action via Save Action Note or Configure Action's `note` field.\n\nUse this endpoint to inspect the full graph of a workflow before modifying individual actions. For a single action in isolation, use Get Action.\n\n**Sheet ID convention:** to operate on the workflow's main sheet, pass the workflow's own ID as `sheet_id` — the main sheet's ID equals the workflow ID. For additional sheets under the workflow, pass the child sheet's ID.","parameters":[{"schema":{"type":"string"},"in":"path","name":"workflow_id","required":true,"description":"Workflow ID."},{"schema":{"type":"string"},"in":"path","name":"sheet_id","required":true,"description":"Sheet ID. To operate on the workflow's main sheet, pass the workflow's ID as `sheet_id` — the main sheet's ID equals the workflow ID."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"type":"array","items":{"$ref":"#/components/schemas/ActionGraphNode"},"description":"Flat array of graph nodes in topological order. First element is always the input node."}}},"example":{"status":200,"data":[{"action_instance_id":"input","action_id":"input","display_name":"Inputs","outputs":[{"name":"linkedin_url","type":"url","reference":"{{input.linkedin_url}}"},{"name":"company_name","type":"string","reference":"{{input.company_name}}"}],"next":["enrich_company_linkedin_profile_1"]},{"action_instance_id":"enrich_company_linkedin_profile_1","action_id":"enrich_company_linkedin_profile","display_name":"Scrape Company LinkedIn Profile","inputs":{"linkedin_url":"{{input.linkedin_url}}"},"outputs":[{"name":"company_name","type":"string","reference":"{{enrich_company_linkedin_profile_1.company_name}}"},{"name":"industry","type":"string","reference":"{{enrich_company_linkedin_profile_1.industry}}"},{"name":"employee_count","type":"number","reference":"{{enrich_company_linkedin_profile_1.employee_count}}"}],"run_if":{"conditions":[{"conditions":[{"variable":"{{input.country}}","operator":"is","values":["US","CA"]}]}],"continue_workflow_if_run_condition_not_met":false},"continue_workflow_if_action_fails":false,"next":["llm_models_1"]},{"action_instance_id":"llm_models_1","action_id":"llm_models","display_name":"AI Generate Content","inputs":{"prompt":"Write a cold email to {{enrich_company_linkedin_profile_1.company_name}} about {{enrich_company_linkedin_profile_1.industry}}.","model":"claude_4_sonnet"},"outputs":[{"name":"generated_content","type":"string","reference":"{{llm_models_1.generated_content}}"}],"next":[]}]}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Workflow or sheet not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Workflow or sheet not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/workflows/{workflow_id}/sheets/{sheet_id}/rows":{"post":{"operationId":"addRows","summary":"Add Rows","tags":["Rows"],"description":"Adds rows to a sheet. Each row's keys are input field names (from Add Inputs); values are written as-is — there is no type checking at the API layer, so any type mismatches surface later when downstream actions try to consume the data. Missing fields are stored as `null`; unknown fields come back in `rejected`.\n\n**Partial success — never atomic.** Accepted rows are written and their UUIDs returned in `row_ids` — pipe directly into Run Rows's `row_ids`. Any rows the server rejects are echoed back in `rejected` with their original content and field-level error codes so you can fix and resend just those. A batch of 10,000 with 5 bad rows writes 9,995 and returns the 5 failures in `rejected`.\n\nMax 1,000 rows per call.\n\n**Sheet ID convention:** to operate on the workflow's main sheet, pass the workflow's own ID as `sheet_id` — the main sheet's ID equals the workflow ID.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddRowsBody"}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"workflow_id","required":true,"description":"Workflow ID."},{"schema":{"type":"string"},"in":"path","name":"sheet_id","required":true,"description":"Sheet ID. To operate on the workflow's main sheet, pass the workflow's ID as `sheet_id` — the main sheet's ID equals the workflow ID."}],"security":[{"apiKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"warnings":{"type":"array","items":{"$ref":"#/components/schemas/RowWarning"},"description":"Non-blocking issues across the batch (e.g. duplicate detection, normalized values). Shape matches Configure Action's warnings."},"data":{"$ref":"#/components/schemas/AddRowsData"}}},"example":{"status":201,"warnings":[],"data":{"row_count":11,"row_ids":["a1b2c3d4-e5f6-7890-abcd-ef1234567001","a1b2c3d4-e5f6-7890-abcd-ef1234567002","a1b2c3d4-e5f6-7890-abcd-ef1234567003","a1b2c3d4-e5f6-7890-abcd-ef1234567004","a1b2c3d4-e5f6-7890-abcd-ef1234567005","a1b2c3d4-e5f6-7890-abcd-ef1234567006","a1b2c3d4-e5f6-7890-abcd-ef1234567007","a1b2c3d4-e5f6-7890-abcd-ef1234567008","a1b2c3d4-e5f6-7890-abcd-ef1234567009","a1b2c3d4-e5f6-7890-abcd-ef1234567010","a1b2c3d4-e5f6-7890-abcd-ef1234567011"],"rejected":[],"rows_queued_for_run":10}}}}},"400":{"description":"Invalid request — e.g. more than 1,000 rows","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Invalid request — e.g. more than 1,000 rows"}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Workflow or sheet not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Workflow or sheet not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/workflows/{workflow_id}/sheets/{sheet_id}/csv":{"post":{"operationId":"importCsv","summary":"Import CSV","tags":["Rows"],"description":"Imports a CSV or XLSX file that has already been uploaded to Floqer into a sheet, without the file's contents passing through the request. Use this instead of Add Rows for files, and for anything above 1,000 rows — it streams the file server-side and has no row limit. Returns 202 immediately; poll Get CSV Import Status for progress. Columns are mapped by header name: `mapping` keys are the file's column headers, values are input field names from List Inputs. Headers are read from row 1 of the file — a header you name that is not there fails the import and says so, rather than importing shifted data. Columns you leave out of `mapping` are not imported, and input fields with no column are left empty. Fields are not created for you; call Add Inputs first if you need them.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportCsvBody"}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"workflow_id","required":true,"description":"Workflow ID."},{"schema":{"type":"string"},"in":"path","name":"sheet_id","required":true,"description":"Sheet ID — rows land on this sheet. To import into the workflow's main sheet, pass the workflow's ID as `sheet_id`; the main sheet's ID equals the workflow ID."}],"security":[{"apiKey":[]}],"responses":{"202":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/ImportCsvData"}}},"example":{"status":202,"data":{"status":"processing","sheet_id":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","columns_mapped":3}}}}},"400":{"description":"Invalid request — `file_ref` is a URL rather than an object key, `mapping` is empty, or it names an input field that is not on this sheet","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Invalid request — `file_ref` is a URL rather than an object key, `mapping` is empty, or it names an input field that is not on this sheet"}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction). Also returned when `file_ref` does not belong to the caller's organization.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction). Also returned when `file_ref` does not belong to the caller's organization."}}}},"404":{"description":"Workflow or sheet not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Workflow or sheet not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}},"get":{"operationId":"getCsvImportStatus","summary":"Get CSV Import Status","tags":["Rows"],"description":"Reports progress of the sheet's most recent CSV import. `processing` means rows are still arriving; `completed` means the import finished and `rows_imported` is final; `failed` carries the reason in `error` — this is where a header named in `mapping` but missing from the file is reported. `unknown` means no recent import status is held for this sheet (status is retained for a limited window after an import finishes).","parameters":[{"schema":{"type":"string"},"in":"path","name":"workflow_id","required":true,"description":"Workflow ID."},{"schema":{"type":"string"},"in":"path","name":"sheet_id","required":true,"description":"Sheet ID — rows land on this sheet. To import into the workflow's main sheet, pass the workflow's ID as `sheet_id`; the main sheet's ID equals the workflow ID."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/ImportCsvStatusData"}}},"example":{"status":200,"data":{"status":"completed","rows_imported":52310}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Workflow or sheet not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Workflow or sheet not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/workflows/{workflow_id}/sheets/{sheet_id}/export":{"post":{"operationId":"exportSheet","summary":"Export Sheet","tags":["Rows"],"description":"Starts a CSV export of the sheet's rows and returns immediately with an `export_id`. The file is built in the background — poll **Get Export** until `status` is `completed`, then download `file_url`.\n\n**Choosing what to export.** Send no body (or `{}`) for the whole sheet. Name rows explicitly with `row_ids`, or narrow with `filters` / `status_filters` / `created_at` — the same shapes List Rows accepts, so a filter that works there selects the same rows here. The two approaches cannot be combined (400): an explicit selection short-circuits the filters in the engine, so mixing them would quietly ignore what you filtered on. `exclude_row_ids` subtracts from the filtered set.\n\n**The output is always CSV**, UTF-8 with a byte-order mark, one column per input field and action output.\n\n**One export at a time.** While an export of this sheet is building for you, starting another returns the in-flight job with `created: false` instead of queueing a second build. Exports are private to the API key's creator — a key created by another member cannot read them.\n\nIds in `row_ids` that are not on the sheet are dropped rather than exported as blank lines — `row_count` reports how many actually matched, so it can be lower than the number you sent.\n\nReturns 400 when nothing matches the selection: an export of zero rows is a mistake worth surfacing, not an empty file.\n\nRequires the `workflows:read` scope.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExportSheetBody"}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"workflow_id","required":true,"description":"Workflow ID."},{"schema":{"type":"string"},"in":"path","name":"sheet_id","required":true,"description":"Sheet ID. To export the workflow's main sheet, pass the workflow's ID as `sheet_id` — the main sheet's ID equals the workflow ID."}],"security":[{"apiKey":[]}],"responses":{"202":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/ExportJob"}},"required":["status","data"]},"example":{"status":202,"data":{"export_id":"8f2c1d90-5a6b-4c7d-8e9f-0a1b2c3d4e5f","status":"running","progress":0,"row_count":15230,"created":true,"started_at":"2026-09-14T13:50:00.000Z","completed_at":null}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Workflow or sheet not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Workflow or sheet not found"}}}},"409":{"description":"An export is already in progress for this sheet, or a delete is running on it. Retry shortly.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"An export is already in progress for this sheet, or a delete is running on it. Retry shortly."}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/workflows/{workflow_id}/sheets/{sheet_id}/export/{export_id}":{"get":{"operationId":"getExport","summary":"Get Export","tags":["Rows"],"description":"Reports the progress of an export and, once it is finished, hands back the download link.\n\n`status` is `running` (queued or building), `completed`, or `failed`. Poll every few seconds; there is no webhook. On `failed`, `error` says why.\n\n**`file_url` appears only when `status` is `completed`.** It is a signed link valid for 15 minutes, minted fresh on each poll — re-poll to get another. Treat it as a secret: it downloads the file without an API key until it expires, so do not log it or paste it anywhere durable.\n\nAn export is readable for **24 hours** by the API key's creator, then forgotten (404). Another member's key cannot read it, and neither can a request naming a different sheet.\n\nRequires the `workflows:read` scope.","parameters":[{"schema":{"type":"string"},"in":"path","name":"workflow_id","required":true,"description":"Workflow ID."},{"schema":{"type":"string"},"in":"path","name":"sheet_id","required":true,"description":"Sheet ID. To export the workflow's main sheet, pass the workflow's ID as `sheet_id` — the main sheet's ID equals the workflow ID."},{"schema":{"type":"string"},"in":"path","name":"export_id","required":true,"description":"The `export_id` returned by Export Sheet."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/ExportJob"}},"required":["status","data"]},"example":{"status":200,"data":{"export_id":"8f2c1d90-5a6b-4c7d-8e9f-0a1b2c3d4e5f","status":"completed","progress":100,"row_count":15230,"file_url":"https://s3.amazonaws.com/…/exports/org_123/8f2c1d90/qualified-leads.csv?X-Amz-Signature=…","expires_at":"2026-09-14T14:05:00.000Z","started_at":"2026-09-14T13:50:00.000Z","completed_at":"2026-09-14T13:52:11.000Z"}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"No such export for this caller, or it has expired","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"No such export for this caller, or it has expired"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/workflows/{workflow_id}/sheets/{sheet_id}/run":{"post":{"operationId":"runRows","summary":"Run Rows","tags":["Rows"],"description":"Runs a subset of the sheet's rows through the action chain. Specify the subset in one of four ways:\n\n- `row_ids` — array of specific row UUIDs (from Add Rows or List Rows). Rows not listed are untouched.\n- `first_10: true` — shortcut for \"run the first 10 rows on the sheet\": the 10 newest, i.e. the top of the table and page 1 of List Rows. Safe experimentation default.\n- `row_range: { start, end }` — rows `start` to `end` (1-indexed, inclusive) in List Rows order, newest first. At most 10,000 rows.\n- `filters` / `status_filters` / `created_at` — every row matching them, selected on the server (the same shapes List Rows takes). Use this instead of listing rows to collect their ids.\n\nExactly one of these must be provided. None, or more than one, returns 400 — except that `row_range` may be sent with the filters to run rows `start` to `end` of the filtered set.\n\n**Asynchronous.** The endpoint returns immediately after queueing. Poll List Rows for per-cell status and outputs — each cell (one action × one row) queues, runs, and bills independently.\n\nTo run every row on the sheet, use Run All Rows instead (requires `confirm_sheet_id` for safety).\n\nPrerequisites: the sheet has at least one action configured, and the rows in `row_ids` (if provided) already exist on the sheet.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunRowsBody"}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"workflow_id","required":true,"description":"Workflow ID."},{"schema":{"type":"string"},"in":"path","name":"sheet_id","required":true,"description":"Sheet ID. To operate on the workflow's main sheet, pass the workflow's ID as `sheet_id` — the main sheet's ID equals the workflow ID."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"message":{"type":"string"},"data":{"$ref":"#/components/schemas/RunRowsData"}}},"example":{"status":200,"message":"Rows queued for execution","data":{"rows_queued":3}}}}},"400":{"description":"Invalid request — e.g. no way of choosing rows given, more than one given, a filter naming an unknown column or action, or a `row_range` that starts past the last row","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Invalid request — e.g. no way of choosing rows given, more than one given, a filter naming an unknown column or action, or a `row_range` that starts past the last row"}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Workflow or sheet not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Workflow or sheet not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/workflows/{workflow_id}/sheets/{sheet_id}/run-all":{"post":{"operationId":"runAllRows","summary":"Run All Rows","tags":["Rows"],"description":"Queues every row on the sheet for execution through the action chain. No request body required — the path is the contract.\n\nSame async semantics as Run Rows — the endpoint returns immediately after queueing; poll List Rows for per-cell status and outputs.\n\n⚠️ **Credit cost.** Scales with `(rows on sheet) × (actions in chain)`. A sheet with 10,000 rows and 5 actions costs 50,000 cell executions. Do not call this without first running a sample (e.g. `run_after_add: \"first_10\"` on Add Rows, or Run Rows with `first_10: true`) and verifying outputs. See the *Run a Workflow* tag for the full build-loop guidance.\n\nSeparate from Run Rows (which takes `row_ids` or `first_10`) so an agent can't accidentally run the whole sheet by misinterpreting a filter. Cache-enabled cells with unchanged resolved inputs still hit cache and don't re-bill.","parameters":[{"schema":{"type":"string"},"in":"path","name":"workflow_id","required":true,"description":"Workflow ID."},{"schema":{"type":"string"},"in":"path","name":"sheet_id","required":true,"description":"Sheet ID. To operate on the workflow's main sheet, pass the workflow's ID as `sheet_id` — the main sheet's ID equals the workflow ID."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"message":{"type":"string"},"data":{"$ref":"#/components/schemas/RunRowsData"}}},"example":{"status":200,"message":"All rows queued for execution","data":{"rows_queued":157}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Workflow or sheet not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Workflow or sheet not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/{action_instance_id}/run":{"post":{"operationId":"runAction","summary":"Run Action","tags":["Rows"],"description":"Runs a specific action against a set of rows. By default the processor runs ONLY the targeted action — downstream actions in the chain are not touched. Set `run_next_action: true` to continue execution down the chain after this action completes, re-running every action that comes after it in the chain.\n\n**Body:** `{ row_ids: [...], run_next_action?: boolean, filters?, status_filters?, created_at? }` or `{ row_range: { start, end }, run_next_action?, filters?, status_filters?, created_at? }`. Unknown fields return 400.\n- `row_ids` — required unless `row_range` is sent. Pass `[]` to queue *every row on the sheet* (analogous to Run All Rows but scoped to one action). Provide explicit IDs (from Add Rows or List Rows) to target a subset.\n- `filters` / `status_filters` / `created_at` — with `row_ids: []`, queue every row matching them, selected on the server (the same shapes List Rows takes). E.g. re-run only the rows whose cell on this action failed, without listing their ids first. Combining them with a non-empty `row_ids` returns 400.\n- `row_range` — instead of `row_ids`, queue rows `start` to `end` (1-indexed, inclusive) in List Rows order, newest first, counted within the filtered rows when filters are sent. At most 10,000 rows. Combining it with a non-empty `row_ids` returns 400.\n- `run_next_action` — `false` (default) runs only the targeted action; `true` continues into the rest of the chain after this action finishes. Useful after Configure Action when you want to refresh a single action (and every action that comes after it) in one call.\n\n**Asynchronous.** Returns immediately after queueing. Poll List Rows for per-cell status and outputs — each cell (one action × one row) queues, runs, and bills independently.\n\n**Credit cost.** Scales with `(rows queued) × (actions actually run)`. With the default `run_next_action: false` that's just the targeted action; with `run_next_action: true` it's the targeted action plus every action downstream of it. Cache-enabled cells whose resolved inputs are unchanged still hit cache and don't re-bill.\n\n**Reserved `action_instance_id` values:** `\"input\"` (the synthetic input pseudo-node) and `\"graph\"` (the graph endpoint path) return 404.\n\n**Sheet ID convention:** to operate on the workflow's main sheet, pass the workflow's own ID as `sheet_id` — the main sheet's ID equals the workflow ID.","requestBody":{"content":{"application/json":{"schema":{"type":"object","description":"Body for Run Action. Send `row_ids` (`[]` queues every row on the sheet, narrowed by the filters when given) or `row_range`; `run_next_action` controls whether the chain continues past the targeted action. Unknown fields return 400.","properties":{"row_ids":{"type":"array","maxItems":1000,"items":{"type":"string","format":"uuid"},"description":"Row UUIDs to queue against the targeted action. From Add Rows (`row_ids` response) or List Rows. Pass `[]` to queue every row on the sheet. Required unless `row_range` is sent.","example":["a1b2c3d4-e5f6-7890-abcd-ef1234567001","a1b2c3d4-e5f6-7890-abcd-ef1234567002"]},"run_next_action":{"type":"boolean","description":"Whether to continue execution down the chain after the targeted action finishes. `false` (default) runs ONLY the targeted action — downstream actions are not touched. `true` runs the targeted action then proceeds through the rest of the chain, re-running every action that comes after it in the chain."},"row_range":{"type":"object","description":"Rows `start` to `end` (1-indexed, inclusive) in List Rows order, newest first — row 1 is the top of the table. With filters, the positions count within the filtered rows. At most 10,000 rows per call. An `end` past the last row runs up to the last row; a `start` past it returns 400.","properties":{"start":{"type":"integer","minimum":1,"description":"First row to run (1-indexed)."},"end":{"type":"integer","minimum":1,"description":"Last row to run (inclusive)."}},"required":["start","end"],"additionalProperties":false,"example":{"start":11,"end":50}},"filters":{"type":"array","description":"Value filters on sheet input columns. Each entry has `variable` (an `{{input.<column>}}` reference), `operator` (one of `is equal to`, `not equal to`, `is empty`, `not empty`, `contains`, `does not contain`, `contains any of`, `not contains any of`, `greater than`, `less than`), and `values` (array of strings — single value for most operators; multiple for `contains any of` / `not contains any of`; empty for `is empty` / `not empty`).","items":{"type":"object","properties":{"variable":{"type":"string","description":"Public reference token for the input column to filter on. Must match `{{input.<column>}}`.","example":"{{input.email}}"},"operator":{"type":"string","description":"Comparison operator. See the description above for the full list.","example":"contains"},"values":{"type":"array","items":{"type":"string"},"description":"Values to compare against. Single-value operators take one item; `contains any of` / `not contains any of` take one or more; `is empty` / `not empty` ignore values.","example":[".com"]}},"required":["variable","operator"]}},"status_filters":{"type":"array","description":"Per-action cell-status filters. Each entry specifies an `action_instance_id` and exactly one of `is` (status matches one of) or `is_not` (status matches none of). Status values use the public vocabulary (`queued | running | complete | failed | error | out_of_credits | condition_not_met`).","items":{"type":"object","properties":{"action_instance_id":{"type":"string","description":"Action instance ID (public form) whose per-row cell status to filter on.","example":"enrich_company_linkedin_profile_1"},"is":{"type":"array","items":{"type":"string"},"description":"Match rows whose cell on this action is in one of these statuses.","example":["complete"]},"is_not":{"type":"array","items":{"type":"string"},"description":"Match rows whose cell on this action is NOT in any of these statuses.","example":["failed","error"]}},"required":["action_instance_id"]}},"created_at":{"type":"object","description":"Filter rows by creation timestamp. `operator` is one of `equals to`, `greater than`, `greater than or equals to`, `less than`, `less than or equals to`, `is between`. `values` is `[ISO timestamp]` for single-value operators or `[start, end]` for `is between`.","properties":{"operator":{"type":"string","example":"greater than"},"values":{"type":"array","items":{"type":"string"},"example":["2026-05-07T22:14:00Z"]}},"required":["operator","values"]}},"additionalProperties":false}}},"description":"Body for Run Action. Send `row_ids` (`[]` queues every row on the sheet, narrowed by the filters when given) or `row_range`; `run_next_action` controls whether the chain continues past the targeted action. Unknown fields return 400."},"parameters":[{"schema":{"type":"string"},"in":"path","name":"workflow_id","required":true,"description":"Workflow ID."},{"schema":{"type":"string"},"in":"path","name":"sheet_id","required":true,"description":"Sheet ID. To operate on the workflow's main sheet, pass the workflow's ID as `sheet_id` — the main sheet's ID equals the workflow ID."},{"schema":{"type":"string"},"examples":{"llm_models_1":{"value":"llm_models_1"},"person_work_email_waterfall_1":{"value":"person_work_email_waterfall_1"}},"in":"path","name":"action_instance_id","required":true,"description":"The action instance to run (and re-run the chain downstream of). Returned by Add Action."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"message":{"type":"string"},"data":{"type":"object","properties":{"rows_queued":{"type":"integer","description":"Number of rows queued for execution against the targeted action. `0` when the sheet has no rows (and `row_ids` was empty / omitted), or when the targeted action is a Blocker, which has nothing to run."}},"required":["rows_queued"]}}},"example":{"status":200,"message":"Rows queued for execution","data":{"rows_queued":3}}}}},"400":{"description":"Invalid request — e.g. neither `row_ids` nor `row_range`, an unknown field, malformed `row_ids` list, `row_ids` combined with filters or `row_range`, a filter naming an unknown column or action, or a `row_range` that starts past the last row","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Invalid request — e.g. neither `row_ids` nor `row_range`, an unknown field, malformed `row_ids` list, `row_ids` combined with filters or `row_range`, a filter naming an unknown column or action, or a `row_range` that starts past the last row"}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Workflow / sheet / action instance not found, or the action_instance_id is reserved (`\"input\"`, `\"graph\"`).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Workflow / sheet / action instance not found, or the action_instance_id is reserved (`\"input\"`, `\"graph\"`)."}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/workflows/{workflow_id}/sheets/{sheet_id}/rows/list":{"post":{"operationId":"listRows","summary":"List Rows","tags":["Rows"],"description":"Returns rows on a sheet with their inputs and per-cell action status/outputs. Use this to poll for results after Run Rows, or to inspect the current state of a sheet.\n\nPOST, not GET — passing up to 200 row UUIDs as a filter doesn't fit reliably in a query string, so the request body carries the filter and pagination.\n\nPass optional `row_ids` to filter — the typical flow after Add Rows or Run Rows when you want only the rows you just touched. Omit `row_ids` to browse all rows on the sheet (paginated).\n\nEach returned row includes the `inputs` you provided and a `cells` object keyed by `action_instance_id` (one entry per action in the chain). For rows that haven't been run yet, `cells` is an empty object `{}`.\n\nEach cell has a `status` that progresses `queued → running → (complete | failed | error | out_of_credits | condition_not_met)`. Complete cells carry their outputs inline as `outputs` when small, or as a URL under `outputs_ref` when the payload is large (e.g. LinkedIn profile scrapes, enrichment blobs — fetch the URL with your API key to read the data). `failed`, `error` and `out_of_credits` cells carry an `error` message. Stop polling on any of them; `out_of_credits` is the one that clears by topping up credits and re-running the row. `queued` and `running` cells carry only `status`.\n\nPaginated — default page size 20, max 200.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListRowsBody"}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"workflow_id","required":true,"description":"Workflow ID."},{"schema":{"type":"string"},"in":"path","name":"sheet_id","required":true,"description":"Sheet ID. To operate on the workflow's main sheet, pass the workflow's ID as `sheet_id` — the main sheet's ID equals the workflow ID."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/ListRowsData"}}},"example":{"status":200,"data":{"rows":[{"row_id":"a1b2c3d4-e5f6-7890-abcd-ef1234567001","row_status":"complete","created_at":"2026-04-20T10:30:00Z","inputs":{"linkedin_url":"https://linkedin.com/company/floqer","email":"hello@floqer.com","company_name":"Floqer"},"cells":{"enrich_company_linkedin_profile_1":{"status":"complete","outputs_ref":{"url":"https://storage.floqer.com/orgs/acme/cells/a1b2c3d4-scrape-1.json","size_bytes":204800}},"llm_models_1":{"status":"complete","outputs":{"generated_content":"Hi Floqer team — noticed you raised a Series A..."}}}},{"row_id":"a1b2c3d4-e5f6-7890-abcd-ef1234567002","row_status":"running","created_at":"2026-04-20T10:30:02Z","inputs":{"linkedin_url":"https://linkedin.com/company/acme","email":"contact@acme.com","company_name":"Acme"},"cells":{"enrich_company_linkedin_profile_1":{"status":"running"},"llm_models_1":{"status":"queued"}}},{"row_id":"a1b2c3d4-e5f6-7890-abcd-ef1234567003","row_status":"has_failures","created_at":"2026-04-20T10:30:05Z","inputs":{"linkedin_url":"not-a-real-url","email":"ops@globex.com","company_name":"Globex"},"cells":{"enrich_company_linkedin_profile_1":{"status":"failed","error":"Provider returned 404 for the given URL."},"llm_models_1":{"status":"failed","error":"Skipped: upstream action enrich_company_linkedin_profile_1 failed."}}}],"total_count":11,"page_no":1,"page_size":20}}}}},"400":{"description":"Invalid request — e.g. `row_ids` length > 200","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Invalid request — e.g. `row_ids` length > 200"}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Workflow or sheet not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Workflow or sheet not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/workflows/{workflow_id}/sheets/{sheet_id}/rows/{row_id}/cells/{action_instance_id}/outputs":{"get":{"operationId":"getCellOutputs","summary":"Get Cell Outputs","tags":["Rows"],"description":"Returns the outputs of one `complete` cell. This is what `outputs_ref.url` on a List Rows cell points at — call it with your API key exactly like any other endpoint.\n\nList Rows inlines a cell's `outputs` when the payload is small and hands back an `outputs_ref` pointing here when it is large (LinkedIn profile scrapes, enrichment blobs). The response shape is the same either way: keys are the action's output field names.\n\nThe URL never expires and carries no credential of its own — it is authorized by your API key on every call, so revoking the key revokes access.\n\nPayload fetches are metered in their own quota (600/minute per key) rather than against the general API rate limit, so following many `outputs_ref` pointers from one page of rows does not exhaust your request budget.","parameters":[{"schema":{"type":"string"},"in":"path","name":"workflow_id","required":true,"description":"Workflow ID."},{"schema":{"type":"string"},"in":"path","name":"sheet_id","required":true,"description":"Sheet ID. To operate on the workflow's main sheet, pass the workflow's ID as `sheet_id` — the main sheet's ID equals the workflow ID."},{"schema":{"type":"string"},"in":"path","name":"row_id","required":true,"description":"Row ID, as returned by List Rows (`row_id`)."},{"schema":{"type":"string"},"in":"path","name":"action_instance_id","required":true,"description":"Action instance ID of the cell — the key it appears under in a row's `cells` map."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"type":"object","properties":{"outputs":{"type":"object","additionalProperties":true,"description":"The cell's outputs. Keys are the action's output field names."}}}}},"example":{"status":200,"data":{"outputs":{"full_name":"Ada Lovelace","headline":"Founder at Analytical Engines","experience":[{"company":"Analytical Engines","title":"Founder"}]}}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Workflow, sheet, row or cell not found — or the cell has no outputs to return (it hasn't completed)","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Workflow, sheet, row or cell not found — or the cell has no outputs to return (it hasn't completed)"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/workflows/{workflow_id}/sheets/{sheet_id}/rows/delete":{"post":{"operationId":"deleteRows","summary":"Delete Rows","tags":["Rows"],"description":"Permanently deletes rows from a sheet. For each deleted row, the input values and all per-cell outputs across every action in the chain are removed. Cannot be undone.\n\n**Partial success — never atomic.** Row UUIDs that exist on the sheet are deleted and returned in `deleted_row_ids`. Any UUIDs the server can't delete (not found on this sheet, malformed) come back in `rejected` with a reason. A batch of 10 with 2 bad UUIDs deletes 8 and reports 2.\n\nMax 200 row UUIDs per call.\n\n**Sheet ID convention:** to operate on the workflow's main sheet, pass the workflow's own ID as `sheet_id` — the main sheet's ID equals the workflow ID.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteRowsBody"}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"workflow_id","required":true,"description":"Workflow ID."},{"schema":{"type":"string"},"in":"path","name":"sheet_id","required":true,"description":"Sheet ID. To operate on the workflow's main sheet, pass the workflow's ID as `sheet_id` — the main sheet's ID equals the workflow ID."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/DeleteRowsData"}}},"example":{"status":200,"data":{"deleted_count":2,"deleted_row_ids":["a1b2c3d4-e5f6-7890-abcd-ef1234567001","a1b2c3d4-e5f6-7890-abcd-ef1234567002"],"rejected":[{"row_id":"a1b2c3d4-e5f6-7890-abcd-ef1234567003","error":"Row not found on this sheet"}]}}}}},"400":{"description":"Invalid request — e.g. more than 200 row IDs","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Invalid request — e.g. more than 200 row IDs"}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Workflow or sheet not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Workflow or sheet not found"}}}},"409":{"description":"A delete, an import or an export is already running on this sheet","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"A delete, an import or an export is already running on this sheet"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/workflows/{workflow_id}/sheets/{sheet_id}/rows/delete-all":{"post":{"operationId":"deleteAllRows","summary":"Delete All Rows","tags":["Rows"],"description":"Permanently deletes every row on the sheet, including all input values and per-cell outputs across every action in the chain. Cannot be undone. Use with extreme caution. No request body required — the path is the contract.\n\nThe delete runs in the background: the call answers `202` with a `job_id`. Poll Get Delete until `status` is `completed` or `failed`. While it runs, Run All, further deletes and CSV imports on the sheet are refused with `409`.\n\n**When to use.** Iterating on a workflow build: add 10 test rows → run → verify → delete all → add real 1,000 rows → run. Separate from Delete Rows (which targets specific row UUIDs) so an agent can't accidentally wipe a sheet by misinterpreting a filter.\n\n**Sheet ID convention:** to operate on the workflow's main sheet, pass the workflow's own ID as `sheet_id` — the main sheet's ID equals the workflow ID.","parameters":[{"schema":{"type":"string"},"in":"path","name":"workflow_id","required":true,"description":"Workflow ID."},{"schema":{"type":"string"},"in":"path","name":"sheet_id","required":true,"description":"Sheet ID. To operate on the workflow's main sheet, pass the workflow's ID as `sheet_id` — the main sheet's ID equals the workflow ID."}],"security":[{"apiKey":[]}],"responses":{"202":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/DeleteAllRowsAccepted"}}},"example":{"status":202,"data":{"status":"processing","job_id":"9f3c1a2b-4d5e-6f70-8a9b-0c1d2e3f4a5b"}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Workflow or sheet not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Workflow or sheet not found"}}}},"409":{"description":"A delete, an import or an export is already running on this sheet","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"A delete, an import or an export is already running on this sheet"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/workflows/{workflow_id}/sheets/{sheet_id}/rows/delete/{job_id}":{"get":{"operationId":"getDelete","summary":"Get Delete","tags":["Rows"],"description":"Progress of a delete started by Delete All Rows. `processing` — rows still being removed. `completed` — finished; `deleted_count` is final. `failed` — see `error`; deleting again finishes the job. Jobs are forgotten after 24 hours.","parameters":[{"schema":{"type":"string"},"in":"path","name":"workflow_id","required":true,"description":"Workflow ID."},{"schema":{"type":"string"},"in":"path","name":"sheet_id","required":true,"description":"Sheet ID. To operate on the workflow's main sheet, pass the workflow's ID as `sheet_id` — the main sheet's ID equals the workflow ID."},{"schema":{"type":"string"},"in":"path","name":"job_id","required":true,"description":"The `job_id` Delete All Rows returned."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/DeleteJobData"}}},"example":{"status":200,"data":{"job_id":"9f3c1a2b-4d5e-6f70-8a9b-0c1d2e3f4a5b","status":"completed","deleted_count":157}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"No such delete on this sheet","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"No such delete on this sheet"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/environment-variables/":{"get":{"operationId":"listEnvironmentVariables","summary":"List Environment Variables","tags":["Environment Variables"],"description":"Returns the organization's environment variables, sorted by `key`. Reference one from any string value in **Configure Action**'s `inputs` using its `reference` token (e.g. `{{env.api_base}}`), on its own or mixed with literal text. The value is substituted for every row when the workflow runs.\n\n**Values are never returned by this API.** Each entry carries the definition and `value_count` — how many scope points have a value — so you can tell a configured variable from an empty one. Values are set in the Floqer app.\n\n**Scopes.** `org` — one value for the whole organization, usable anywhere. `user` — one value per member, resolved against the workflow's owner (not whoever triggers the run), usable anywhere. `workflow` — one value per workflow, so it resolves ONLY in workflows that have a value set for it.\n\n**Pass `workflow_id`** to get the list usable in that workflow: workflow-scoped variables with no value there are omitted. Without it you get the org-wide inventory, which can include workflow-scoped variables that resolve nowhere you are currently building.\n\nEnvironment variables cannot be used in conditions (`run_if`, or the Filter action's `path_conditions`) — they are substituted after conditions are evaluated. Configure Action returns a warning and skips the condition if you try.\n\nRequires the `workflows:read` scope.","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"query","name":"workflow_id","required":false,"description":"Restrict the list to variables usable in this workflow — drops workflow-scoped variables that have no value set for it. A sheet ID also works; it resolves to its parent workflow."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"type":"array","items":{"$ref":"#/components/schemas/EnvironmentVariable"},"description":"The organization's environment variables, sorted by `key`."}},"required":["status","data"]},"example":{"status":200,"data":[{"key":"api_base","name":"API Base","description":"Base URL for our internal API.","scope":"org","reference":"{{env.api_base}}","value_count":1,"created_by":"admin@acme.com","updated_at":"2026-07-30T15:41:40.634Z"},{"key":"prospeo_key","name":"Prospeo-Key","description":null,"scope":"workflow","reference":"{{env.prospeo_key}}","value_count":3,"created_by":"ops@acme.com","updated_at":"2026-09-02T22:43:26.490Z"}]}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/usage/credits":{"get":{"operationId":"getUsageCredits","summary":"Get Credit Balance","tags":["Usage"],"description":"Returns the organization's spendable credit balance — what is left to spend right now, not what has been spent. For spend, use Get Usage Overview.\n\nThe balance is organization-wide and is the same for every key in the organization, including a key narrowed to its creator's own usage.\n\n**It is the organization's balance, not this caller's allowance.** A member can also carry a personal credit quota, which is enforced when a run consumes credits, not here — so a run can still be refused for quota while this reports credits available. Treat it as the ceiling, not a guarantee.\n\nThe balance is read from the credit ledger, while every breakdown endpoint in this module reads an analytics replica of the same ledger. The replica lags by seconds, so a run that has just completed can be reflected in this balance a moment before it appears in a breakdown.\n\nRequires the `billing:read` scope, which every role holds.","security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/UsageCredits"}},"required":["status","data"]},"example":{"status":200,"data":{"credits":16134.5}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/usage/overview":{"get":{"operationId":"getUsageOverview","summary":"Get Usage Overview","tags":["Usage"],"description":"Returns usage for the requested window: totals, a per-day trend, and the top workflows, actions and members by credits spent. Defaults to the current calendar month (UTC).\n\n**What a run is.** One run is one action call against one row. A run served from cache is counted in `total_runs` and `cache_hits` but costs no credits; `cache_savings` reports what those runs would have cost.\n\n`top_workflows` lists workflow sheets only. Source spend counts toward `summary` — which is the whole organization's usage — and is attributed by List Source Usage.\n\n**What the key can see.** A key holding `org:read` (admin only) reports the whole organization and populates `top_users` and `summary.active_users`. Any other key reports only the usage of the member who created it, leaves `top_users` empty, and omits `active_users`.\n\n| `code` | Status | Meaning |\n|---|---|---|\n| `LEDGER_CUTOVER_SPAN` | 400 | The window crosses the date this organization's usage history moved stores, and cannot be reported as one figure. Request the window on one side of `cutover_at`. |\n\nRequires the `billing:read` scope, which every role holds.","parameters":[{"schema":{"type":"string"},"in":"query","name":"from","required":false,"description":"Inclusive start of the window, ISO 8601. Defaults to the first day of the current UTC month."},{"schema":{"type":"string"},"in":"query","name":"to","required":false,"description":"Exclusive end of the window, ISO 8601. Defaults to now."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/UsageOverview"}},"required":["status","data"]}}}},"400":{"description":"Bad Request — `from` or `to` is not a parsable ISO 8601 date, `from` is later than `to`, or the window crosses the usage ledger cutover.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"},"cutover_at":{"type":"string","description":"The cutover instant, ISO 8601. Request a window entirely before or entirely after it."}},"description":"Bad Request — `from` or `to` is not a parsable ISO 8601 date, `from` is later than `to`, or the window crosses the usage ledger cutover."}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/usage/workflows":{"get":{"operationId":"listUsageWorkflows","summary":"List Workflow Usage","tags":["Usage"],"description":"Returns credits spent per workflow sheet in the requested window, ranked by credits. Defaults to the current calendar month (UTC).\n\n**One row is one SHEET, not one workflow.** A workflow with three sheets produces three rows sharing a `workflow_id`; sum them for the workflow total. Names match the Sheet schema — `sheet_id` is the row, `workflow_id` is the workflow it belongs to — so either can be passed straight to the workflow endpoints. `is_main_sheet` marks the workflow's own main sheet, whose `sheet_id` equals its `workflow_id`.\n\nCapped at the 100 highest-spending sheets, and not paginated. Narrow the window to see further down.\n\nSources are reported separately by List Source Usage, not here.\n\nDeleted workflows still appear with `deleted: true` — their runs really did spend credits in the window, so dropping them would under-report the total.\n\n**What the key can see.** A key holding `org:read` (admin only) reports the whole organization. Any other key reports only the usage of the member who created it, so a sheet a colleague ran is absent.\n\n| `code` | Status | Meaning |\n|---|---|---|\n| `LEDGER_CUTOVER_SPAN` | 400 | The window crosses the date this organization's usage history moved stores. Request the window on one side of `cutover_at`. |\n\nRequires the `billing:read` scope, which every role holds.","parameters":[{"schema":{"type":"string"},"in":"query","name":"from","required":false,"description":"Inclusive start of the window, ISO 8601. Defaults to the first day of the current UTC month."},{"schema":{"type":"string"},"in":"query","name":"to","required":false,"description":"Exclusive end of the window, ISO 8601. Defaults to now."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"type":"array","items":{"$ref":"#/components/schemas/UsageWorkflowRow"},"description":"One row per sheet with activity in the window, highest spend first."}},"required":["status","data"]}}}},"400":{"description":"Bad Request — `from` or `to` is not a parsable ISO 8601 date, `from` is later than `to`, or the window crosses the usage ledger cutover.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"},"cutover_at":{"type":"string","description":"The cutover instant, ISO 8601. Request a window entirely before or entirely after it."}},"description":"Bad Request — `from` or `to` is not a parsable ISO 8601 date, `from` is later than `to`, or the window crosses the usage ledger cutover."}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/usage/workflows/{sheet_id}":{"get":{"operationId":"getUsageWorkflow","summary":"Get Workflow Usage","tags":["Usage"],"description":"Returns one sheet's usage in the requested window: totals, a per-day trend, the actions that spent the credits, the most frequent errors, the 20 most recent runs, and per-member spend. Defaults to the current calendar month (UTC).\n\n`sheet_id` is one SHEET's id, as returned by List Workflow Usage — the same id the workflow endpoints take as `{sheet_id}`. Passing a workflow's main-sheet id reports that sheet alone, not the workflow's other sheets; for a workflow total, sum its rows in List Workflow Usage. An id that is not a UUID is a 400, so a 404 always means the window holds no usage for that sheet.\n\nA key restricted by `allowed_workflow_ids` may only drill into sheets of the workflows on its list; the list endpoints are not filtered that way, because a filtered list would not add up to its own totals.\n\n**What the key can see.** A key holding `org:read` (admin only) reports every member's runs and populates `per_user`. Any other key reports only the runs of the member who created it and leaves `per_user` empty — so a sheet only a colleague ran returns 404 for that key, even though the sheet exists.\n\n| `code` | Status | Meaning |\n|---|---|---|\n| `LEDGER_CUTOVER_SPAN` | 400 | The window crosses the date this organization's usage history moved stores. Request the window on one side of `cutover_at`. |\n\nRequires the `billing:read` scope, which every role holds.","parameters":[{"schema":{"type":"string"},"in":"query","name":"from","required":false,"description":"Inclusive start of the window, ISO 8601. Defaults to the first day of the current UTC month."},{"schema":{"type":"string"},"in":"query","name":"to","required":false,"description":"Exclusive end of the window, ISO 8601. Defaults to now."},{"schema":{"type":"string","format":"uuid"},"in":"path","name":"sheet_id","required":true,"description":"Sheet ID, as returned by List Workflow Usage."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/UsageWorkflowDetail"}},"required":["status","data"]}}}},"400":{"description":"Bad Request — `from` or `to` is not a parsable ISO 8601 date, `from` is later than `to`, or the window crosses the usage ledger cutover.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"},"cutover_at":{"type":"string","description":"The cutover instant, ISO 8601. Request a window entirely before or entirely after it."}},"description":"Bad Request — `from` or `to` is not a parsable ISO 8601 date, `from` is later than `to`, or the window crosses the usage ledger cutover."}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Not Found — no usage for this sheet in the window, or it is not visible to this key.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Not Found — no usage for this sheet in the window, or it is not visible to this key."}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/usage/sources":{"get":{"operationId":"listUsageSources","summary":"List Source Usage","tags":["Usage"],"description":"Returns credits spent per source instance in the requested window, ranked by credits. Defaults to the current calendar month (UTC).\n\n`source_instance_id` is the same id `GET /api/v1/sources` returns under that name — the source INSTANCE, not the `source_id` source-type slug. Pass it to `GET /api/v1/sources/{source_instance_id}/data` to see what the spend produced.\n\nSource runs are reported here and excluded from List Workflow Usage, so the two never double-count.\n\nNot paginated, and capped at 100 rows — but that cap is applied to workflow sheets and sources together before they are split, so an organization whose highest spenders are all sheets can see fewer than 100 sources here, or none at all. Narrow the window to bring sources into the top 100.\n\n**What the key can see.** A key holding `org:read` (admin only) reports the whole organization. Any other key reports only the usage of the member who created it.\n\n| `code` | Status | Meaning |\n|---|---|---|\n| `LEDGER_CUTOVER_SPAN` | 400 | The window crosses the date this organization's usage history moved stores. Request the window on one side of `cutover_at`. |\n\nRequires the `billing:read` scope, which every role holds.","parameters":[{"schema":{"type":"string"},"in":"query","name":"from","required":false,"description":"Inclusive start of the window, ISO 8601. Defaults to the first day of the current UTC month."},{"schema":{"type":"string"},"in":"query","name":"to","required":false,"description":"Exclusive end of the window, ISO 8601. Defaults to now."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"type":"array","items":{"$ref":"#/components/schemas/UsageSourceRow"},"description":"One row per source with activity in the window, highest spend first."}},"required":["status","data"]}}}},"400":{"description":"Bad Request — `from` or `to` is not a parsable ISO 8601 date, `from` is later than `to`, or the window crosses the usage ledger cutover.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"},"cutover_at":{"type":"string","description":"The cutover instant, ISO 8601. Request a window entirely before or entirely after it."}},"description":"Bad Request — `from` or `to` is not a parsable ISO 8601 date, `from` is later than `to`, or the window crosses the usage ledger cutover."}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/sources/{source_id}/preview":{"post":{"operationId":"previewSource","summary":"Preview Source","tags":["Sources"],"description":"Preview the records that would be ingested for the given source payload, without persisting anything. Use this to validate filters / list selections before committing to Create.\n\nPer-source body shapes and field semantics live in `/docs/source-detail/{source_id}.txt`. The route schema only enforces that the body is a JSON object; per-source validation runs inside the handler and surfaces as 400 with a specific message and, when multiple fields fail, an `errors[]` array of `{field, message}` entries.\n\nResults are typically capped at 100 rows (source-specific — e.g. Slack returns up to 10). `metadata.total_results` is the count returned in this preview; it is omitted when the source cannot count the matches. When `metadata.capped` is true, more rows exist upstream but were omitted due to preview limits.\n\n**Trimming the response.** Pass a top-level `limit` (positive integer) to cap how many rows come back — useful when you only need to eyeball the shape of a row rather than the whole sample. `limit` is a response-shaping knob applied by this endpoint: it does not reduce what the source fetches upstream, and it never changes `metadata.total_results`. When it drops rows, `metadata.capped` is true.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcePreviewBody"}}}},"parameters":[{"schema":{"type":"string"},"examples":{"find_companies":{"value":"find_companies"},"find_companies_by_tech_stack":{"value":"find_companies_by_tech_stack"},"find_companies_from_sales_navigator":{"value":"find_companies_from_sales_navigator"},"find_companies_hiring":{"value":"find_companies_hiring"},"find_ecommerce_stores":{"value":"find_ecommerce_stores"},"find_job_postings":{"value":"find_job_postings"},"find_linkedin_post_reactors":{"value":"find_linkedin_post_reactors"},"find_linkedin_posts":{"value":"find_linkedin_posts"},"find_people":{"value":"find_people"},"find_people_from_sales_navigator":{"value":"find_people_from_sales_navigator"},"import_from_ai_ark":{"value":"import_from_ai_ark"},"import_from_attio":{"value":"import_from_attio"},"import_from_fireflies":{"value":"import_from_fireflies"},"import_from_gmail":{"value":"import_from_gmail"},"import_from_google_sheets":{"value":"import_from_google_sheets"},"import_from_hubspot":{"value":"import_from_hubspot"},"import_from_pipedrive":{"value":"import_from_pipedrive"},"import_from_salesforce":{"value":"import_from_salesforce"},"import_from_slack":{"value":"import_from_slack"},"import_from_snowflake":{"value":"import_from_snowflake"},"import_from_stripe":{"value":"import_from_stripe"},"import_from_typeform":{"value":"import_from_typeform"},"import_from_zoho":{"value":"import_from_zoho"},"search_local_businesses":{"value":"search_local_businesses"},"search_x_tweets":{"value":"search_x_tweets"},"track_companies_hiring":{"value":"track_companies_hiring"},"track_job_postings":{"value":"track_job_postings"},"track_linkedin_posts":{"value":"track_linkedin_posts"},"track_personal_website_visitors":{"value":"track_personal_website_visitors"},"track_website_visitors":{"value":"track_website_visitors"},"track_x_posts":{"value":"track_x_posts"}},"in":"path","name":"source_id","required":true,"description":"Source identifier. Currently supported: `find_companies`, `find_companies_by_tech_stack`, `find_companies_from_sales_navigator`, `find_companies_hiring`, `find_ecommerce_stores`, `find_job_postings`, `find_linkedin_post_reactors`, `find_linkedin_posts`, `find_people`, `find_people_from_sales_navigator`, `import_from_ai_ark`, `import_from_attio`, `import_from_fireflies`, `import_from_gmail`, `import_from_google_sheets`, `import_from_hubspot`, `import_from_pipedrive`, `import_from_salesforce`, `import_from_slack`, `import_from_snowflake`, `import_from_stripe`, `import_from_typeform`, `import_from_zoho`, `search_local_businesses`, `search_x_tweets`, `track_companies_hiring`, `track_job_postings`, `track_linkedin_posts`, `track_personal_website_visitors`, `track_website_visitors`, `track_x_posts`."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/SourcePreviewResponse"}}}}}},"400":{"description":"Per-source validation failed (missing or invalid payload fields).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"errors":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"message":{"type":"string"}},"required":["field","message"]}}},"required":["error","message"],"description":"Per-source validation failed (missing or invalid payload fields)."}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Unknown `source_id` — see the param description for supported slugs.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unknown `source_id` — see the param description for supported slugs."}}}},"424":{"description":"The user is not connected to the integration this source depends on.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"The user is not connected to the integration this source depends on."}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/sources/{source_id}":{"post":{"operationId":"createSource","summary":"Create Source","tags":["Sources"],"description":"Create a new source. Behavior is source-specific: typically the source imports matching records immediately (when `pull_existing` or equivalent is set) and — for an ongoing source — keeps re-importing on a schedule.\n\nPer-source body shapes and field semantics live in `/docs/source-detail/{source_id}.txt`. The route schema only enforces that the body is a JSON object; per-source validation runs inside the handler and surfaces as 400 with a specific message and, when multiple fields fail, an `errors[]` array of `{field, message}` entries.\n\nReturns `source_instance_id` (the new source's UUID). The initial import runs asynchronously: the call returns as soon as the source is created and the import is queued, not when records have finished arriving.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourceCreateBody"}}}},"parameters":[{"schema":{"type":"string"},"examples":{"find_companies":{"value":"find_companies"},"find_companies_by_tech_stack":{"value":"find_companies_by_tech_stack"},"find_companies_from_sales_navigator":{"value":"find_companies_from_sales_navigator"},"find_companies_hiring":{"value":"find_companies_hiring"},"find_ecommerce_stores":{"value":"find_ecommerce_stores"},"find_job_postings":{"value":"find_job_postings"},"find_linkedin_post_reactors":{"value":"find_linkedin_post_reactors"},"find_linkedin_posts":{"value":"find_linkedin_posts"},"find_people":{"value":"find_people"},"find_people_from_sales_navigator":{"value":"find_people_from_sales_navigator"},"import_from_ai_ark":{"value":"import_from_ai_ark"},"import_from_attio":{"value":"import_from_attio"},"import_from_fireflies":{"value":"import_from_fireflies"},"import_from_gmail":{"value":"import_from_gmail"},"import_from_google_sheets":{"value":"import_from_google_sheets"},"import_from_hubspot":{"value":"import_from_hubspot"},"import_from_pipedrive":{"value":"import_from_pipedrive"},"import_from_salesforce":{"value":"import_from_salesforce"},"import_from_slack":{"value":"import_from_slack"},"import_from_snowflake":{"value":"import_from_snowflake"},"import_from_stripe":{"value":"import_from_stripe"},"import_from_typeform":{"value":"import_from_typeform"},"import_from_zoho":{"value":"import_from_zoho"},"search_local_businesses":{"value":"search_local_businesses"},"search_x_tweets":{"value":"search_x_tweets"},"track_companies_hiring":{"value":"track_companies_hiring"},"track_job_postings":{"value":"track_job_postings"},"track_linkedin_posts":{"value":"track_linkedin_posts"},"track_personal_website_visitors":{"value":"track_personal_website_visitors"},"track_website_visitors":{"value":"track_website_visitors"},"track_x_posts":{"value":"track_x_posts"}},"in":"path","name":"source_id","required":true,"description":"Source identifier. Currently supported: `find_companies`, `find_companies_by_tech_stack`, `find_companies_from_sales_navigator`, `find_companies_hiring`, `find_ecommerce_stores`, `find_job_postings`, `find_linkedin_post_reactors`, `find_linkedin_posts`, `find_people`, `find_people_from_sales_navigator`, `import_from_ai_ark`, `import_from_attio`, `import_from_fireflies`, `import_from_gmail`, `import_from_google_sheets`, `import_from_hubspot`, `import_from_pipedrive`, `import_from_salesforce`, `import_from_slack`, `import_from_snowflake`, `import_from_stripe`, `import_from_typeform`, `import_from_zoho`, `search_local_businesses`, `search_x_tweets`, `track_companies_hiring`, `track_job_postings`, `track_linkedin_posts`, `track_personal_website_visitors`, `track_website_visitors`, `track_x_posts`."}],"security":[{"apiKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/SourceCreateResponse"}}}}}},"400":{"description":"Per-source validation failed (missing or invalid payload fields).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"errors":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"message":{"type":"string"}},"required":["field","message"]}}},"required":["error","message"],"description":"Per-source validation failed (missing or invalid payload fields)."}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"402":{"description":"Credit consumption failed — the source row was archived and the initial pull did not start. Surface the message to the user; they likely need to top up.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Credit consumption failed — the source row was archived and the initial pull did not start. Surface the message to the user; they likely need to top up."}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Unknown `source_id` — see the param description for supported slugs.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unknown `source_id` — see the param description for supported slugs."}}}},"424":{"description":"The user is not connected to the integration this source depends on.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"The user is not connected to the integration this source depends on."}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/sources/{source_instance_id}/sync":{"post":{"operationId":"syncSource","summary":"Sync Source to Workflow","tags":["Sources"],"description":"Connect a created source to a workflow and (by default) backfill the workflow with the source's existing rows. `source_instance_id` is the UUID returned by Create.\n\n`field_mapping` keys are public workflow input references (`input.<field>`); each value is the source field NAME to pull — the row's top-level key exactly as it appears in the Preview response (e.g. `Name`, `email`, `business_id`), NEVER a nested path like `Name.value`. The import resolves each field's value automatically, including for sources whose preview cells render as `{label, value}`. Each key is resolved to the matching workflow input. Keys are CASE-SENSITIVE — use the exact lowercase snake_case `reference` from List Inputs (minus the `{{ }}`); variants like `Input.Email` or `input.firstName` return 400. Surrounding `{{ }}` is tolerated.\n\n`push_existing` (default true) toggles the backfill. Whether landed rows execute the workflow — backfilled and future rows alike — is the sheet's `auto_run` toggle: set it with Update Sheet (`PATCH /api/v1/workflows/{workflow_id}/sheets/{sheet_id}`) before syncing, and use Run Rows for explicit `row_ids`. The legacy `run` field is deprecated: it is accepted, ignored, and reported in `warnings[]`.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SyncSourceBody"}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"source_instance_id","required":true,"description":"Source instance UUID (the `source_instance_id` returned by Create)."}],"security":[{"apiKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"warnings":{"type":"array","description":"Non-blocking issues, e.g. a deprecated body field that was ignored.","items":{"type":"object","properties":{"field":{"type":"string"},"code":{"type":"string"},"message":{"type":"string"}}}},"data":{"$ref":"#/components/schemas/SyncSourceResponse"}}}}}},"400":{"description":"Invalid body, or a field_mapping key doesn't match a workflow input.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Invalid body, or a field_mapping key doesn't match a workflow input."}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Source not found (or not owned by the caller's org).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Source not found (or not owned by the caller's org)."}}}},"409":{"description":"The source is already connected to this workflow. Update the existing connection instead of syncing again.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"The source is already connected to this workflow. Update the existing connection instead of syncing again."}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/sources/{source_instance_id}/data":{"get":{"operationId":"getSourceData","summary":"Get Source Data","tags":["Sources"],"description":"Returns paginated rows that have been imported into a created source. `source_instance_id` is the UUID returned by Create.\n\nUse this to inspect or poll a source after Create while its backfill runs, or to page through the full dataset before syncing to a workflow.\n\nQuery params `page_no` (1-indexed, default 1) and `page_size` (default 20, max 200) control pagination. `total_count` is the total number of rows stored for the source.","parameters":[{"schema":{"type":"string"},"in":"query","name":"page_no","required":false,"description":"1-indexed page number. Defaults to 1."},{"schema":{"type":"string"},"in":"query","name":"page_size","required":false,"description":"Rows per page. Defaults to 20; capped at 200."},{"schema":{"type":"string"},"in":"path","name":"source_instance_id","required":true,"description":"Source instance UUID (the `source_instance_id` returned by Create)."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/GetSourceDataResponse"}}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Source not found (or not owned by the caller's org).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Source not found (or not owned by the caller's org)."}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/sources/{source_instance_id}/status":{"patch":{"operationId":"patchSourceStatus","summary":"Pause or Resume Source","tags":["Sources"],"description":"Pause or resume a created source. `source_instance_id` is the UUID returned by Create.\n\nSet `status` to `paused` to stop an ongoing source's recurring runs, or `active` to resume it. Pausing stops the schedule (when present), disables provider webhooks where applicable (e.g. Stripe, TheirStack), and prevents further imports; resuming restarts them. Static / one-time sources have nothing recurring to pause, but the status still updates.\n\nReturns the source's new public status.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchSourceStatusBody"}}}},"parameters":[{"schema":{"type":"string","format":"uuid","pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"},"in":"path","name":"source_instance_id","required":true,"description":"Source instance UUID (the `source_instance_id` returned by Create)."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/PatchSourceStatusResponse"}}}}}},"400":{"description":"Invalid `source_instance_id` or `status`.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Invalid `source_instance_id` or `status`."}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Source not found (or not owned by the caller's org).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Source not found (or not owned by the caller's org)."}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/sources/{source_id}/options/{field_name}":{"post":{"operationId":"getSourceOptions","summary":"Get Source Field Options","tags":["Sources"],"description":"Fetches the dropdown / dynamic option values for one of a source's payload fields. Use it when building a source's preview / create payload whose field expects a value from a connected integration (HubSpot lists, HubSpot properties per object type, etc.).\n\nPass the `source_id` (same identifier used by the source's preview / create endpoints, e.g. `import_from_hubspot`) and the `field_name`. Behaves like the workflow Get Action Field Options endpoint, so resolver behavior is consistent across the two surfaces.\n\nCascading resolvers (e.g. HubSpot: pick `object_type` → list `hubspot_properties`) take their parent selection via `context`. Calling a cascading resolver without the required key returns a 400 with the accepted key spellings. `hubspot_lists` takes an optional `context.search` (words matched against list names) to find a list by name without pulling the whole catalog.\n\nReturns options as `{ value, label, extras? }`. `value` is what to send back in the source's payload; `extras` carries per-integration metadata when relevant.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetSourceOptionsBody"}}}},"parameters":[{"schema":{"type":"string"},"example":"import_from_hubspot","in":"path","name":"source_id","required":true,"description":"Source identifier — same as on the source's preview / create endpoint."},{"schema":{"type":"string"},"examples":{"hubspot_lists":{"value":"hubspot_lists"},"hubspot_properties":{"value":"hubspot_properties"}},"in":"path","name":"field_name","required":true,"description":"Snake-cased payload field name to fetch options for. Matches the keys on the source's preview / create body."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/GetSourceOptionsData"}}},"example":{"status":200,"data":{"options":[{"value":"123","label":"All Customers"},{"value":"456","label":"Trial Signups - Last 30d"}]}}}}},"400":{"description":"Invalid request — e.g. the field has no resolver for that source, or a cascading resolver was called without its required `context` keys.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Invalid request — e.g. the field has no resolver for that source, or a cascading resolver was called without its required `context` keys."}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"424":{"description":"The user is not connected to the underlying integration (e.g. no HubSpot OAuth). Surface as a 'connect first' CTA.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"The user is not connected to the underlying integration (e.g. no HubSpot OAuth). Surface as a 'connect first' CTA."}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}},"502":{"description":"Upstream integration call failed (provider down, expired token, etc.).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Upstream integration call failed (provider down, expired token, etc.)."}}}}}}},"/api/v1/sources/":{"get":{"operationId":"listSources","summary":"List Sources","tags":["Sources"],"description":"List the sources you've created, newest first. Only sources whose type is supported by this API are returned (data feeds not exposed by this API are omitted).\n\nEach entry's `source_instance_id` is the source INSTANCE UUID — pass it to `POST /api/v1/sources/{source_instance_id}/sync` to connect it to a workflow. `source_id` is the source-type slug used by the preview / create / options endpoints.","security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"type":"array","items":{"$ref":"#/components/schemas/SourceListItem"},"description":"The caller's sources, newest first."}}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/agent/run":{"post":{"operationId":"runAgent","summary":"Run Agent","tags":["Agent"],"description":"Runs a single Floqer Nova web-agent request synchronously and returns the result. Provide a natural-language `prompt` and, optionally, an `output_schema` describing the JSON fields you want back. Requests typically take 10-60 seconds; the request stays open until the agent finishes. Billing: one Floqer Nova charge per request. Any 5xx response is automatically refunded — a 5xx means you were not charged. Every response (success or error) includes a `request_id` (e.g. `fl_…`) — quote it when contacting support. Error codes: INVALID_REQUEST, INVALID_OUTPUT_SCHEMA (400); UNAUTHORIZED (401); INSUFFICIENT_CREDITS (402); ROLE_FORBIDDEN, INSUFFICIENT_SCOPE (403); RATE_LIMITED (429); AGENT_INTERNAL_ERROR, AGENT_EXECUTION_FAILED (500); AGENT_BUSY (503); AGENT_TIMEOUT (504).","requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["prompt"],"properties":{"prompt":{"type":"string","minLength":1,"maxLength":20000,"description":"Natural-language instruction for the web agent."},"output_schema":{"type":"object","description":"Optional JSON-schema-style object describing the structured output. Shape: `{ \"properties\": { \"<field>\": { \"type\": \"string\", \"description\": \"...\" } } }`. Supported field types: string, number, integer, boolean, array, object. Max 20 fields. When omitted, the full answer is returned under a single `result` field."}}}}},"required":true},"security":[{"apiKey":[]}],"responses":{"200":{"description":"Agent completed successfully.","content":{"application/json":{"schema":{"type":"object","description":"Agent completed successfully.","properties":{"status":{"type":"integer"},"data":{"type":"object","additionalProperties":true,"description":"Structured output keyed by your output_schema fields, or `{ result }` when no schema given."},"citations":{"type":"array","items":{"type":"object","additionalProperties":true},"description":"Web sources the agent consulted."},"request_id":{"type":"string","description":"Unique id for this request (e.g. fl_…). Quote it when contacting support."}}}}}},"400":{"description":"Invalid request body or output schema.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string","description":"Machine-readable error code"},"request_id":{"type":"string","description":"Unique id for this request (e.g. fl_…). Quote it when contacting support."}},"description":"Invalid request body or output schema."}}}},"401":{"description":"Unauthorized (UNAUTHORIZED) — missing or invalid API key.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string","description":"Machine-readable error code"},"request_id":{"type":"string","description":"Unique id for this request (e.g. fl_…). Quote it when contacting support."}},"description":"Unauthorized (UNAUTHORIZED) — missing or invalid API key."}}}},"402":{"description":"Insufficient credits — INSUFFICIENT_CREDITS.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string","description":"Machine-readable error code"},"request_id":{"type":"string","description":"Unique id for this request (e.g. fl_…). Quote it when contacting support."}},"description":"Insufficient credits — INSUFFICIENT_CREDITS."}}}},"403":{"description":"Your role (ROLE_FORBIDDEN) or the API key (INSUFFICIENT_SCOPE) lacks the agent:run scope.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string","description":"Machine-readable error code"},"request_id":{"type":"string","description":"Unique id for this request (e.g. fl_…). Quote it when contacting support."}},"description":"Your role (ROLE_FORBIDDEN) or the API key (INSUFFICIENT_SCOPE) lacks the agent:run scope."}}}},"429":{"description":"Rate limited (RATE_LIMITED) — agent calls have their own org-level quota, separate from general API limits.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[1]}},"description":"Number of seconds to wait before retrying"}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string","description":"Machine-readable error code"},"request_id":{"type":"string","description":"Unique id for this request (e.g. fl_…). Quote it when contacting support."},"retryAfter":{"type":"integer","description":"Seconds until the rate limit window resets."}},"description":"Rate limited (RATE_LIMITED) — agent calls have their own org-level quota, separate from general API limits."}}}},"500":{"description":"Agent failed (AGENT_INTERNAL_ERROR / AGENT_EXECUTION_FAILED). Not charged.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string","description":"Machine-readable error code"},"request_id":{"type":"string","description":"Unique id for this request (e.g. fl_…). Quote it when contacting support."}},"description":"Agent failed (AGENT_INTERNAL_ERROR / AGENT_EXECUTION_FAILED). Not charged."}}}},"503":{"description":"Agent at capacity (AGENT_BUSY). Not charged; retry after `Retry-After`.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string","description":"Machine-readable error code"},"request_id":{"type":"string","description":"Unique id for this request (e.g. fl_…). Quote it when contacting support."}},"description":"Agent at capacity (AGENT_BUSY). Not charged; retry after `Retry-After`."}}}},"504":{"description":"Agent timed out (AGENT_TIMEOUT). Not charged.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string","description":"Machine-readable error code"},"request_id":{"type":"string","description":"Unique id for this request (e.g. fl_…). Quote it when contacting support."}},"description":"Agent timed out (AGENT_TIMEOUT). Not charged."}}}}}}},"/api/v1/shortcuts/floqer":{"get":{"operationId":"listFloqerShortcuts","summary":"List Floqer Shortcuts","tags":["Shortcuts"],"description":"Browse globally shared Floqer shortcuts. Use each ID with the normal shortcut detail and run endpoints. Publishing requires shortcuts:write and verified Floqer staff or platform-admin authority.","security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"type":"object","properties":{"shortcuts":{"type":"array","items":{"$ref":"#/components/schemas/FloqerShortcut"}},"enabled":{"type":"boolean"},"can_publish":{"type":"boolean"}}}}}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}},"503":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}}}},"post":{"operationId":"publishFloqerShortcut","summary":"Publish Floqer Shortcut","tags":["Shortcuts"],"description":"Share an ordinary shortcut across organizations. Its current published version becomes available through normal shortcut endpoints. Future source publications apply automatically; repeating this request returns the existing entry. source_shortcut_id is the ID returned by public List/Create Shortcut. Requires shortcuts:write and verified Floqer publishing authority.","requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["source_shortcut_id"],"properties":{"source_shortcut_id":{"type":"string","pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"},"name":{"type":"string","minLength":1,"maxLength":160,"pattern":"\\S"},"description":{"type":"string","maxLength":4000}}}}},"required":true},"security":[{"apiKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"type":"object","properties":{"shortcut":{"$ref":"#/components/schemas/FloqerShortcut"}}}}}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}},"503":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}}}}},"/api/v1/shortcuts/floqer/{floqer_shortcut_id}":{"patch":{"operationId":"setFloqerShortcutListed","summary":"List or Unlist Floqer Shortcut","tags":["Shortcuts"],"description":"Staff can unlist a shortcut to stop discovery and new runs, or list it again. Accepted runs and the caller's history remain available.","requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["is_listed"],"properties":{"is_listed":{"type":"boolean"}}}}},"required":true},"parameters":[{"schema":{"type":"string","pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"},"in":"path","name":"floqer_shortcut_id","required":true}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"type":"object","properties":{"shortcut":{"$ref":"#/components/schemas/FloqerShortcut"}}}}}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}},"503":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}}}}},"/api/v1/shortcuts/":{"post":{"operationId":"createShortcut","summary":"Create Shortcut","tags":["Shortcuts"],"description":"Packages a workflow as a shortcut and publishes its first version, in one call:\n\n- **input_schema** — the workflow's input columns (List Inputs). Every column becomes an input; its `reference` is the snake_cased column name.\n- **output_schema** — the `outputs` you send. Each item names one field a run returns and points it at a value, usually a `{{...}}` reference to an upstream action. They are written onto the workflow's `output_shortcut` action, which is added to the end of the chain when there is none.\n\n**Requirements** (a 400 lists every unmet one in `reasons`): the workflow has at least one named input column, and at least one item in `outputs`. You must be able to edit the workflow. A workflow with one `output_shortcut` action per branch cannot be packaged here; use the Floqer app.\n\nOne shortcut per workflow: a second call returns 409. The created shortcut is published and runnable immediately — pass its `id` to Run Shortcut. To change its outputs or pick up new input columns later, call Update Shortcut; editing the workflow alone does not change the shortcut.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateShortcutBody"}}}},"security":[{"apiKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"message":{"type":"string"},"warnings":{"type":"array","description":"Non-fatal issues with `outputs` — an unresolved `{{...}}` reference an item with neither a name nor a value, or an extension turned off because its input was removed (`extension_input_removed`). Same codes as Configure Action. Absent when every item applied cleanly.","items":{"type":"object","properties":{"field":{"type":"string"},"code":{"type":"string"},"message":{"type":"string"}}}},"data":{"$ref":"#/components/schemas/Shortcut"}}}}}},"400":{"description":"Bad Request — invalid body (`error: \"Validation Error\"` with per-field `errors[]`), the workflow has more than one `output_shortcut` action, or it cannot back a shortcut yet (`code: \"SHORTCUT_WORKFLOW_INELIGIBLE\"` with `reasons[]`), or `extensions` names an input the shortcut does not have","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"errors":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"message":{"type":"string"}},"required":["field","message"]}},"code":{"type":"string"},"reasons":{"type":"array","items":{"type":"string"},"description":"Every unmet requirement, in plain language. Present with code `SHORTCUT_WORKFLOW_INELIGIBLE`."}},"required":["error","message"],"description":"Bad Request — invalid body (`error: \"Validation Error\"` with per-field `errors[]`), the workflow has more than one `output_shortcut` action, or it cannot back a shortcut yet (`code: \"SHORTCUT_WORKFLOW_INELIGIBLE\"` with `reasons[]`), or `extensions` names an input the shortcut does not have"}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Workflow not found in your organization (`code: \"WORKFLOW_NOT_FOUND\"`)","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Workflow not found in your organization (`code: \"WORKFLOW_NOT_FOUND\"`)"}}}},"409":{"description":"Conflict — this workflow already has a shortcut (`code: \"SHORTCUT_ALREADY_EXISTS\"`), or its draft already holds an unpublished `output_shortcut` action (`code: \"OUTPUT_NODE_EXISTS\"`), which has to be published or removed in the Floqer app first.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Conflict — this workflow already has a shortcut (`code: \"SHORTCUT_ALREADY_EXISTS\"`), or its draft already holds an unpublished `output_shortcut` action (`code: \"OUTPUT_NODE_EXISTS\"`), which has to be published or removed in the Floqer app first."}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}},"get":{"operationId":"listShortcuts","summary":"List Shortcuts","tags":["Shortcuts"],"description":"Returns all shortcuts accessible to the authenticated user. Optionally filter by ownership type: `master` (shortcuts you created), `shared` (shortcuts shared with you or published to your whole organization), or `all` (both). Defaults to all. Use `search` to find shortcuts by what they do: it matches their name, description, inputs and outputs. Shortcuts with `callable: true` can be called from a workflow through the `call_shortcut` action.","parameters":[{"schema":{"type":"string","enum":["master","shared","all"]},"in":"query","name":"filter","required":false,"description":"Filter shortcuts by ownership type"},{"schema":{"type":"string","maxLength":200},"example":"enrich company","in":"query","name":"search","required":false,"description":"Words to find, case-insensitive. A shortcut matches when every word appears, in any order, in its name, description, or any input (name, `reference`, description) or output (name, description). Shortcuts whose name contains every word come first."}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Shortcut"}}}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/shortcuts/{id}":{"get":{"operationId":"getShortcut","summary":"Get Shortcut","tags":["Shortcuts"],"description":"Returns full details for a single shortcut. `input_schema` lists the fields to pass as `input_data` to Run Shortcut; `output_schema` lists the fields returned in a run's `output_data`.\n\n`visibility` says whether the whole organization can use it. `shared_users` lists the people it is shared with, for the shortcut's creator (Share Shortcut); other callers get `[]`.","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true,"description":"App ID"}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/Shortcut"}}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Shortcut not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Shortcut not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}},"patch":{"operationId":"updateShortcut","summary":"Update Shortcut","tags":["Shortcuts"],"description":"Changes a shortcut you created. Send only the fields you want to change.\n\n- `name`, `description`, `icon_emoji` — metadata, applied immediately.\n- `visibility` — `org` lets everyone in your organization use the shortcut; `private` limits it to you and the people it is shared with (Share Shortcut). Applied immediately, with no new version.\n- `outputs` — the full list of fields a run returns, in the same shape Create Shortcut takes. It replaces the previous outputs, is written onto the workflow's `output_shortcut` action, and publishes a **new version** of the shortcut. The version is re-derived from the workflow, so input columns added since the last publish are picked up at the same time; every existing input keeps its type, required flag and default. An output whose `name` is unchanged keeps its identity.\n\nA shortcut is frozen at publish: editing the workflow on its own (Configure Action, Add Inputs) never changes what the shortcut takes or returns. This is the call that does.\n\nFor a shortcut shared globally by Floqer, this changes the source version used by new global runs. It requires verified Floqer publishing authority; accepted runs retain their original version.\n\nMaking a shortcut `private` while another workflow's Call Shortcut cell still calls it is refused with 409 `SHORTCUT_HAS_CALLERS`, naming the callers; send `force: true` to make it private anyway and break those calls.\n\nRemoving an output that another workflow's Call Shortcut cell reads is refused with 409 `SHORTCUT_CONTRACT_BREAK`, naming the outputs and the callers; send `force: true` to publish anyway and mark those call sites broken. A refused call changes nothing — not the workflow, not the shortcut.\n\nRuns already in flight finish on the version they started on.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateShortcutBody"}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true,"description":"App ID"}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"message":{"type":"string"},"warnings":{"type":"array","description":"Non-fatal issues with `outputs` — an unresolved `{{...}}` reference an item with neither a name nor a value, or an extension turned off because its input was removed (`extension_input_removed`). Same codes as Configure Action. Absent when every item applied cleanly.","items":{"type":"object","properties":{"field":{"type":"string"},"code":{"type":"string"},"message":{"type":"string"}}}},"data":{"$ref":"#/components/schemas/Shortcut"}}}}}},"400":{"description":"Bad Request — invalid body (`error: \"Validation Error\"` with per-field `errors[]`), the workflow has more than one `output_shortcut` action, or it can no longer back a shortcut (`code: \"SHORTCUT_WORKFLOW_INELIGIBLE\"` with `reasons[]`), or `extensions` names an input the shortcut does not have","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"errors":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"message":{"type":"string"}},"required":["field","message"]}},"code":{"type":"string"},"reasons":{"type":"array","items":{"type":"string"},"description":"Every unmet requirement, in plain language. Present with code `SHORTCUT_WORKFLOW_INELIGIBLE`."}},"required":["error","message"],"description":"Bad Request — invalid body (`error: \"Validation Error\"` with per-field `errors[]`), the workflow has more than one `output_shortcut` action, or it can no longer back a shortcut (`code: \"SHORTCUT_WORKFLOW_INELIGIBLE\"` with `reasons[]`), or `extensions` names an input the shortcut does not have"}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Shortcut not found, archived, or not one you created","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Shortcut not found, archived, or not one you created"}}}},"409":{"description":"Conflict — the new `outputs` break existing call sites (`code: \"SHORTCUT_CONTRACT_BREAK\"`, `breaks[]` + `callers`; send `force: true` to proceed), `visibility: \"private\"` would break other workflows' calls (`code: \"SHORTCUT_HAS_CALLERS\"` + `callers`; send `force: true` to proceed), the shortcut predates versioning and must be republished in the Floqer app first (`code: \"SHORTCUT_NOT_PUBLISHED\"`), or the workflow's draft holds an unpublished `output_shortcut` action to publish or remove first (`code: \"OUTPUT_NODE_EXISTS\"`)","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"},"breaks":{"type":"array","description":"What the new contract removes or tightens. Present with `SHORTCUT_CONTRACT_BREAK`.","items":{"type":"object","properties":{"kind":{"type":"string","description":"`output_removed`, `input_required_added` or `input_required_tightened`."},"field":{"type":"string","description":"The output's `name`, or the input's column name."},"message":{"type":"string"}}}},"callers":{"type":"object","description":"Everything that reads this shortcut. Present with `SHORTCUT_CONTRACT_BREAK` and `SHORTCUT_HAS_CALLERS`.","properties":{"workflows":{"type":"array","items":{"type":"object","properties":{"workflow_id":{"type":"string"},"workflow_name":{"type":"string"},"action_instance_id":{"type":"string","description":"The Call Shortcut action in that workflow."},"published":{"type":"boolean","description":"false when the call is still in an unpublished draft."}}}},"ackdb_columns":{"type":"array","items":{"type":"object","properties":{"watcher_connection_id":{"type":"integer"},"workflow_id":{"type":"string"},"watcher_id":{"type":"string","nullable":true},"segment_id":{"type":"string","nullable":true}}}}}}},"description":"Conflict — the new `outputs` break existing call sites (`code: \"SHORTCUT_CONTRACT_BREAK\"`, `breaks[]` + `callers`; send `force: true` to proceed), `visibility: \"private\"` would break other workflows' calls (`code: \"SHORTCUT_HAS_CALLERS\"` + `callers`; send `force: true` to proceed), the shortcut predates versioning and must be republished in the Floqer app first (`code: \"SHORTCUT_NOT_PUBLISHED\"`), or the workflow's draft holds an unpublished `output_shortcut` action to publish or remove first (`code: \"OUTPUT_NODE_EXISTS\"`)"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/shortcuts/{id}/shares":{"patch":{"operationId":"shareShortcut","summary":"Share Shortcut","tags":["Shortcuts"],"description":"Lets members of your organization use the shortcut, and stops letting others, in one call. It does not give access to the workflow behind it; share the workflow for that. To open it to the whole organization instead, set `visibility` to `org` with Update Shortcut.\n\nOnly the shortcut's creator can do this. You, and people in `add` who already have it, are reported in `data.skipped`; `data.failed` lists shares that could not be saved, so retry those. People in `remove` who did not have it are ignored. Read the current list from Get Shortcut (`shared_users`).\n\n**Check what removed people keep.** They can still use the shortcut while it is visible to the whole organization (`data.organization_access_remains`) or while they can edit its workflow (`data.workflow_edit_access_remains` lists them). To fully revoke, also set `visibility` to `private` or unshare the workflow.\n\nA refusal changes nothing; its `code` says why, and `emails` lists any refused addresses.","requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"minProperties":1,"properties":{"add":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"string","format":"email"},"description":"Email addresses to share with. They must be current members of your organization.","example":["sam@acme.com"]},"remove":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"string","minLength":1},"description":"Email addresses to stop sharing with. Former members can be removed too.","example":["kim@acme.com"]}}}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true,"description":"App ID"}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"message":{"type":"string"},"data":{"type":"object","properties":{"shortcut_id":{"type":"string"},"shared_users":{"type":"array","items":{"type":"string"},"description":"Everyone the shortcut is shared with, after this call."},"added":{"type":"array","items":{"type":"string"},"description":"Newly shared with."},"skipped":{"type":"array","items":{"type":"string"},"description":"You, and people who already had it."},"failed":{"type":"array","items":{"type":"string"},"description":"Shares that could not be saved; retry them."},"removed":{"type":"array","items":{"type":"string"},"description":"No longer shared with."},"organization_access_remains":{"type":"boolean","description":"The shortcut is visible to the whole organization, so removed people can still use it."},"workflow_edit_access_remains":{"type":"array","items":{"type":"string"},"description":"Removed people who can still use it because they can edit its workflow."}}}}}}}},"400":{"description":"Bad Request — invalid body, or a refused change (`code` + `emails`)","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"errors":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"message":{"type":"string"}},"required":["field","message"]}},"code":{"type":"string"},"emails":{"type":"array","items":{"type":"string"}}},"required":["error","message"],"description":"Bad Request — invalid body, or a refused change (`code` + `emails`)"}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Shortcut not found, archived, or not one you created","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Shortcut not found, archived, or not one you created"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/shortcuts/{id}/run":{"post":{"operationId":"runShortcut","summary":"Run Shortcut","tags":["Shortcuts"],"description":"Starts a shortcut run with the provided input data. The shortcut must be published. Key `input_data` by each field's `reference` value from the shortcut's `input_schema` (see Get Shortcut); unknown keys are rejected with a 400 listing the valid references. The run is asynchronous — poll the Get Shortcut Run endpoint for status and results.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["input_data"],"properties":{"input_data":{"type":"object","additionalProperties":true,"description":"Input field values keyed by each field's `reference` from the shortcut's `input_schema`"}}}}},"required":true},"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true,"description":"App ID"}],"security":[{"apiKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"message":{"type":"string"},"data":{"type":"object","properties":{"id":{"type":"string"},"data_id":{"type":"string"},"status":{"type":"string","enum":["pending","completed","failed","payloadFilled","readyToBeReviewed","checkingNextSource","conditionNotMet","error","outOfCredits","duplicate"]},"created_at":{"type":"string","format":"date-time"}}}}},"example":{"status":201,"message":"Shortcut run started","data":{"id":"f1a2b3c4-d5e6-7f8a-9b0c-1d2e3f4a5b6c","data_id":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","status":"pending","created_at":"2025-01-15T10:30:00Z"}}}}},"400":{"description":"Invalid input fields","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Invalid input fields"}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction). Also returned when the shortcut is unpublished.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction). Also returned when the shortcut is unpublished."}}}},"404":{"description":"Shortcut not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Shortcut not found"}}}},"409":{"description":"Conflict — the shortcut is archived or has no published version (`code: \"SHORTCUT_ARCHIVED\"` / `\"SHORTCUT_NOT_PUBLISHED\"`)","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Conflict — the shortcut is archived or has no published version (`code: \"SHORTCUT_ARCHIVED\"` / `\"SHORTCUT_NOT_PUBLISHED\"`)"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}},"503":{"description":"Service Unavailable — the shortcut's frozen graph is momentarily unreadable (`code: \"SHORTCUT_GRAPH_UNAVAILABLE\"`); retry","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Service Unavailable — the shortcut's frozen graph is momentarily unreadable (`code: \"SHORTCUT_GRAPH_UNAVAILABLE\"`); retry"}}}}}}},"/api/v1/shortcuts/{id}/run/batch":{"post":{"operationId":"runShortcutBatch","summary":"Run Shortcut Batch","tags":["Shortcuts"],"description":"Starts one shortcut run per input row, grouped under a single `batch_id`. The shortcut must be published. Key each row by the field `reference` values from the shortcut's `input_schema` (see Get Shortcut); unknown keys are rejected with a 400 listing the valid references. Accepts up to 1000 rows per request. Runs execute asynchronously — poll List Shortcut Runs with the returned `batch_id` for status and results.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["rows"],"properties":{"rows":{"type":"array","minItems":1,"maxItems":1000,"items":{"type":"object","additionalProperties":true},"description":"Input rows — one shortcut run is created per row. Each row is keyed by field `reference` values from the shortcut's `input_schema`."}}}}},"required":true},"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true,"description":"App ID"}],"security":[{"apiKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"message":{"type":"string"},"data":{"type":"object","properties":{"batch_id":{"type":"string","description":"ID grouping every run created by this request — filter List Shortcut Runs by it"},"run_count":{"type":"integer","description":"Number of runs created"}}}}},"example":{"status":201,"message":"Shortcut batch run started","data":{"batch_id":"3f2a1b6c-9d4e-4f7a-8b0c-5d6e7f8a9b0c","run_count":25}}}}},"400":{"description":"Invalid input fields","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Invalid input fields"}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction). Also returned when the shortcut is unpublished.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction). Also returned when the shortcut is unpublished."}}}},"404":{"description":"Shortcut not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Shortcut not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/shortcuts/{id}/run/csv":{"post":{"operationId":"runShortcutCsv","summary":"Run Shortcut from CSV","tags":["Shortcuts"],"description":"Runs the shortcut once per row of a CSV or XLSX file that has already been uploaded to Floqer, without the file's contents passing through the request. Use this instead of Run Shortcut Batch for files, and for anything above 1,000 rows — it streams the file server-side and has no row limit. Returns 202 with a `batch_id`; the runs appear on List Shortcut Runs filtered by that `batch_id` as the file streams, so an empty list right after this call means the import has not reached its first row yet. Columns are mapped by header name: `mapping` keys are the file's column headers, values are input `reference` values from Get Shortcut. Headers are read from row 1 — a header you name that is not there fails the import and reports which one, rather than running every row with shifted inputs. Columns you leave out of `mapping` are not sent; inputs with no column fall back to the shortcut's `defaultValue`, then to \"\".","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunShortcutCsvBody"}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true,"description":"Shortcut ID."}],"security":[{"apiKey":[]}],"responses":{"202":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"message":{"type":"string"},"data":{"$ref":"#/components/schemas/RunShortcutCsvData"}}},"example":{"status":202,"message":"Shortcut CSV run started","data":{"status":"processing","batch_id":"b1c2d3e4-f5a6-7890-abcd-ef1234567890","columns_mapped":3}}}}},"400":{"description":"Invalid request — `file_ref` is a URL rather than an object key, `mapping` is empty, or it names an input reference this shortcut does not have","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Invalid request — `file_ref` is a URL rather than an object key, `mapping` is empty, or it names an input reference this shortcut does not have"}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction). Also returned when the shortcut is unpublished, or when `file_ref` does not belong to the caller's organization.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction). Also returned when the shortcut is unpublished, or when `file_ref` does not belong to the caller's organization."}}}},"404":{"description":"Shortcut not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Shortcut not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/shortcuts/{id}/runs":{"get":{"operationId":"listShortcutRuns","summary":"List Shortcut Runs","tags":["Shortcuts"],"description":"Returns paginated runs for a shortcut. Filter by `mode` (single/batch) or `batch_id` to narrow results. Uses limit/offset pagination (default: 50 items, offset 0). For filtered queries `total` reflects the returned page's row count — paginate until a page returns fewer rows than `limit`.","parameters":[{"schema":{"type":"string"},"in":"query","name":"limit","required":false,"description":"Number of runs to return (default: 50)"},{"schema":{"type":"string"},"in":"query","name":"offset","required":false,"description":"Offset for pagination (default: 0)"},{"schema":{"type":"string","enum":["single","batch"]},"in":"query","name":"mode","required":false,"description":"Filter by run mode"},{"schema":{"type":"string"},"in":"query","name":"batch_id","required":false,"description":"Filter by batch ID"},{"schema":{"type":"string","enum":["true","false"]},"in":"query","name":"include_steps","required":false,"description":"Include read-only action outputs for each global shortcut run on this page. Defaults to false."},{"schema":{"type":"string"},"in":"query","name":"origin_workflow_id","required":false,"description":"Calling workflow. Supply all three origin fields together."},{"schema":{"type":"string"},"in":"query","name":"origin_data_id","required":false,"description":"Calling workflow row."},{"schema":{"type":"string"},"in":"query","name":"origin_action_id","required":false,"description":"Calling shortcut action."},{"schema":{"type":"string"},"in":"path","name":"id","required":true,"description":"App ID"}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"type":"object","properties":{"runs":{"type":"array","items":{"$ref":"#/components/schemas/ShortcutRun"}},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer"}}}}}}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Shortcut not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Shortcut not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/shortcuts/{id}/runs/{runId}":{"get":{"operationId":"getShortcutRun","summary":"Get Shortcut Run","tags":["Shortcuts"],"description":"Returns details for a single shortcut run, including input data, output data, and current status. For in-progress runs, the status is fetched live from the underlying workflow engine — so this endpoint reflects real-time progress. Poll this endpoint to wait for completion.","parameters":[{"schema":{"type":"string","enum":["true","false"]},"in":"query","name":"include_steps","required":false,"description":"Include read-only step results from the executed global shortcut version."},{"schema":{"type":"string"},"in":"path","name":"id","required":true,"description":"Shortcut ID"},{"schema":{"type":"string"},"in":"path","name":"runId","required":true,"description":"Run ID"}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/ShortcutRun"}}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Shortcut or run not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Shortcut or run not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/shortcuts/{id}/runs/export":{"post":{"operationId":"exportShortcutRuns","summary":"Export Shortcut Runs","tags":["Shortcuts"],"description":"Starts a CSV export of one batch's runs and returns immediately with an `export_id`. The file is built in the background — poll **Get Shortcut Runs Export** until `status` is `completed`, then download `file_url`.\n\n`batch_id` is the one returned by **Run Shortcut Batch** or **Import CSV for Shortcuts**, and the same id **List Shortcut Runs** filters by.\n\n**Columns** are one per input field, then one per output field, then `status` — the same public field names the run endpoints return. They are fixed when the export starts, so a batch whose later runs failed still produces a rectangular file, and editing the shortcut afterwards cannot reshape an export already under way.\n\n**One export at a time.** While an export of this shortcut is building for you, starting another returns the in-flight job with `created: false` instead of queueing a second build, so a retry is safe.\n\nExports are private to the API key's creator, and so are the runs themselves — you can only export a batch you ran. A batch with no runs is a 400 rather than an empty file.\n\nRequires the `shortcuts:read` scope.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExportShortcutRunsBody"}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true,"description":"App ID"}],"security":[{"apiKey":[]}],"responses":{"202":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/ShortcutExportJob"}},"required":["status","data"]},"example":{"status":202,"data":{"export_id":"8f2c1d90-5a6b-4c7d-8e9f-0a1b2c3d4e5f","status":"running","progress":0,"row_count":240,"created":true,"started_at":"2026-09-15T09:10:00.000Z","completed_at":null}}}}},"400":{"description":"`batch_id` missing, or the batch has no runs","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"`batch_id` missing, or the batch has no runs"}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Shortcut not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Shortcut not found"}}}},"409":{"description":"An export is already in progress — retry shortly","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"An export is already in progress — retry shortly"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/shortcuts/{id}/runs/export/{export_id}":{"get":{"operationId":"getShortcutRunsExport","summary":"Get Shortcut Runs Export","tags":["Shortcuts"],"description":"Reports the progress of an export started by **Export Shortcut Runs** and, once finished, hands back the download link.\n\n`status` is `running` (queued or building), `completed`, or `failed`. Poll every few seconds; there is no webhook. On `failed`, `error` says why.\n\n**`file_url` appears only when `status` is `completed`.** It is a signed link valid for 15 minutes, minted fresh on each poll — re-poll for another if it expires. Treat it as a secret: it downloads the file without an API key for as long as it is valid.\n\nAn export is readable for **24 hours**, then forgotten (404). An `export_id` belonging to another shortcut, or to another member's key, also answers 404.\n\nRequires the `shortcuts:read` scope.","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true,"description":"Shortcut ID"},{"schema":{"type":"string"},"in":"path","name":"export_id","required":true,"description":"The `export_id` returned by Export Shortcut Runs"}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/ShortcutExportJob"}},"required":["status","data"]},"example":{"status":200,"data":{"export_id":"8f2c1d90-5a6b-4c7d-8e9f-0a1b2c3d4e5f","status":"completed","progress":100,"row_count":240,"file_url":"https://s3.amazonaws.com/…/exports/org_123/8f2c1d90/shortcut-runs.csv?X-Amz-Signature=…","expires_at":"2026-09-15T09:27:00.000Z","started_at":"2026-09-15T09:10:00.000Z","completed_at":"2026-09-15T09:12:04.000Z"}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"No such export for this caller, or it has expired","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"No such export for this caller, or it has expired"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/campaigns/sequence/catalog":{"get":{"operationId":"listCampaignNodeCatalog","summary":"List Sequence Step Types","tags":["Campaigns"],"description":"Returns the step types available to a campaign sequence. Pass an entry's `id` as `type` in Add Node. Entries where `default_config` is applied on creation; fields marked `required` are enforced. `start` and `wait` are listed for reference but cannot be added — `start` is seeded automatically and `wait` steps are managed for you between steps.","security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"type":"array","items":{"$ref":"#/components/schemas/CampaignNodeCatalogEntry"}}}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/campaigns/{campaign_id}/sequence":{"get":{"operationId":"getCampaignSequence","summary":"Get Campaign Sequence","tags":["Campaigns"],"description":"Returns the campaign's sequence steps in execution order, starting at the `start` step and following `next_node_id`. Returns the draft by default; pass `published=true` for the live graph. Only the published graph is executed.\n\n`source` says which copy you got; `has_published_sequence` says whether a published copy exists; `has_unpublished_changes` says whether a publish is pending.","parameters":[{"schema":{"type":"string","enum":["true","false"]},"in":"query","name":"published","required":false,"description":"`true` returns the live (published) graph; default `false` returns the draft"},{"schema":{"type":"string"},"in":"path","name":"campaign_id","required":true,"description":"Campaign ID"}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"type":"object","properties":{"nodes":{"type":"array","items":{"$ref":"#/components/schemas/CampaignSequenceNode"}},"source":{"type":"string","enum":["draft","published"],"description":"Which copy these steps came from — echoes the `published` query param"},"has_published_sequence":{"type":"boolean","description":"Whether this campaign has a published sequence at all"},"has_unpublished_changes":{"type":"boolean","description":"The draft has moved ahead of the published graph"}}}}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/campaigns/{campaign_id}/lead-schema":{"patch":{"operationId":"updateCampaignLeadSchema","summary":"Update Lead Schema","tags":["Campaigns"],"description":"Registers the fields a lead can carry. Add Leads ignores any key that isn't in the schema, so call this first when pushing leads with custom fields.\n\n**Additive** — a field you don't mention is kept, so a partial write never wipes the rest. A new `name` creates a field; re-sending an existing one is a no-op. A field's `type` is locked once created (a different type is ignored rather than rewriting stored values), and `email` and `name` are fixed fields that cannot be renamed, retyped or removed.\n\nThe schema is written to the draft, so fields registered here survive the next publish, and to the live sequence when one already exists. Registering fields never publishes a sequence on its own. Returns the full merged schema.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["fields"],"properties":{"fields":{"type":"array","minItems":1,"description":"Fields to register. Existing fields not listed here are left untouched.","items":{"type":"object","required":["name"],"properties":{"name":{"type":"string","description":"Field name — the key to use for this field when adding leads","example":"company"},"type":{"type":"string","description":"Field type, applied on creation only. Defaults to `string`.","example":"string"}}}}}}}},"required":true},"parameters":[{"schema":{"type":"string"},"in":"path","name":"campaign_id","required":true,"description":"Campaign ID"}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"message":{"type":"string"},"data":{"type":"object","properties":{"campaign_id":{"type":"string"},"fields":{"type":"array","items":{"$ref":"#/components/schemas/CampaignLeadField"},"description":"The campaign's full lead schema after the merge"}}}}}}}},"400":{"description":"No fields given, or a name collides with an existing field","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"No fields given, or a name collides with an existing field"}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Campaign not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Campaign not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/campaigns/{campaign_id}/sequence/nodes":{"post":{"operationId":"addCampaignSequenceNode","summary":"Add Sequence Step","tags":["Campaigns"],"description":"Adds a step to the campaign's draft sequence and returns it. Appends to the end by default; pass `after` (a `node_id`) to insert after a specific step.\n\n`type` is a catalog `id` from List Sequence Step Types. `config` is merged over that type's `default_config` — required fields must end up set (a `send` step needs `subject` and `body`).\n\nWait steps between steps are managed automatically and cannot be added directly; patch a wait step's `config` to change a delay.\n\n**This writes the draft.** Call Publish Sequence to make it live.\n\n**Concurrent writes are safe** — overlapping calls to one campaign serialize, and every change is kept. A sequence builder open in the browser saves its own copy of the whole graph, so an edit made through the API while that canvas is open is overwritten by the next canvas save.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["type"],"properties":{"type":{"type":"string","enum":["send","manual_email","manual_task","phone_call_task","linkedin_message_task"],"description":"Step type — a catalog `id` from List Sequence Step Types"},"after":{"type":"string","description":"Insert immediately after this `node_id`. Omit to append to the end of the sequence."},"config":{"type":"object","additionalProperties":true,"description":"Step configuration, merged over the type's `default_config`"},"name":{"type":"string","description":"Custom display name for this step"}}}}},"required":true},"parameters":[{"schema":{"type":"string"},"in":"path","name":"campaign_id","required":true,"description":"Campaign ID"}],"security":[{"apiKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"message":{"type":"string"},"data":{"$ref":"#/components/schemas/CampaignSequenceNode"}}},"example":{"status":201,"message":"Step added","data":{"node_id":"send-m2k9x1p-0","type":"send","display_name":null,"config":{"subject":"Quick question","body":"Hi {{name}} …","sender_mode":"pool"},"next_node_id":null}}}}},"400":{"description":"Unknown step type or invalid config, or `after` names a step that is not connected to the sequence","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unknown step type or invalid config, or `after` names a step that is not connected to the sequence"}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Campaign or `after` step not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Campaign or `after` step not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/campaigns/{campaign_id}/sequence/nodes/{node_id}":{"patch":{"operationId":"updateCampaignSequenceNode","summary":"Update Sequence Step","tags":["Campaigns"],"description":"Updates a step's configuration or display name in the draft sequence. `config` is merged over the existing config — send only the keys you're changing. Patch a `wait` step's `delay` and `unit` to change the gap between two steps.\n\n**This writes the draft.** Call Publish Sequence to make it live.\n\n**Concurrent writes are safe** — overlapping calls to one campaign serialize, and every change is kept. A sequence builder open in the browser saves its own copy of the whole graph, so an edit made through the API while that canvas is open is overwritten by the next canvas save.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"config":{"type":"object","additionalProperties":true,"description":"Config keys to merge over the step's existing config"},"name":{"type":"string","nullable":true,"description":"Display name; pass null to clear it"}}}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"campaign_id","required":true,"description":"Campaign ID"},{"schema":{"type":"string"},"in":"path","name":"node_id","required":true,"description":"Step ID"}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"message":{"type":"string"},"data":{"$ref":"#/components/schemas/CampaignSequenceNode"}}}}}},"400":{"description":"Invalid config for this step type, or `config.leadFields` (use Update Lead Schema instead)","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Invalid config for this step type, or `config.leadFields` (use Update Lead Schema instead)"}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Campaign or step not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Campaign or step not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}},"delete":{"operationId":"deleteCampaignSequenceNode","summary":"Delete Sequence Step","tags":["Campaigns"],"description":"Removes a step from the draft sequence, closing the chain around it and keeping the delay above it. The `start` step cannot be deleted.\n\n**This writes the draft.** Call Publish Sequence to make it live — at which point any lead sitting on the removed step completes.\n\n**Concurrent writes are safe** — overlapping calls to one campaign serialize, and every change is kept. A sequence builder open in the browser saves its own copy of the whole graph, so an edit made through the API while that canvas is open is overwritten by the next canvas save.","parameters":[{"schema":{"type":"string"},"in":"path","name":"campaign_id","required":true,"description":"Campaign ID"},{"schema":{"type":"string"},"in":"path","name":"node_id","required":true,"description":"Step ID"}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"message":{"type":"string"},"data":{"type":"object","properties":{"campaign_id":{"type":"string"},"node_id":{"type":"string"},"deleted":{"type":"boolean"}}}}}}}},"400":{"description":"The start step cannot be deleted","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"The start step cannot be deleted"}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Campaign or step not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Campaign or step not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/campaigns/{campaign_id}/sequence/publish":{"post":{"operationId":"publishCampaignSequence","summary":"Publish Sequence","tags":["Campaigns"],"description":"Publishes the draft sequence, making it the graph the campaign runs. Also enrols any leads that were added before a sequence existed. Steps keep their ids across publishes, so in-flight leads resume where they were.\n\nEvery step is re-validated first — publishing is the last gate before real emails go out, and a step written from the campaign builder never passed Add or Update Step. A step missing a required field (a `send` with no `subject` or `body`, say) fails the publish with a 400 naming it, rather than shipping a blank email.\n\nThe campaign still needs `state` to be `active` to send — use Resume Campaign.","parameters":[{"schema":{"type":"string"},"in":"path","name":"campaign_id","required":true,"description":"Campaign ID"}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"message":{"type":"string"},"data":{"type":"object","properties":{"campaign_id":{"type":"string"},"published":{"type":"boolean"},"steps":{"type":"integer","description":"Steps in the published sequence"},"enrolled":{"type":"integer","description":"Leads enrolled by this publish"}}}}}}}},"400":{"description":"The draft sequence has no steps, or a step is missing a required config field","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"The draft sequence has no steps, or a step is missing a required config field"}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Campaign not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Campaign not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/campaigns/{campaign_id}/leads":{"post":{"operationId":"addCampaignLeads","summary":"Add Leads","tags":["Campaigns"],"description":"Adds leads to a campaign. Each lead needs an `email`; any other keys must match the campaign's `lead_schema` (see Get Campaign) and are otherwise ignored and reported in `dropped_fields` — register custom fields first with Update Lead Schema. Re-adding an existing address updates it instead of duplicating, so retries are safe. Accepts up to 1000 leads per call.\n\nRows without a usable `email` are **skipped, not fatal** — they are counted in `skipped` and the rest of the batch is still added. The call only fails if no row has one.\n\n**Enrolment.** Leads enrol as soon as the campaign has a **published sequence**, whether or not it is active — a lead added to a paused campaign becomes `in_sequence` with its next step held, and starts moving on Resume Campaign. Leads only stay at `added` when no sequence is published yet; Publish Sequence then picks them up.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["leads"],"properties":{"leads":{"type":"array","minItems":1,"maxItems":1000,"items":{"type":"object","additionalProperties":true,"properties":{"email":{"type":"string","description":"Lead email address — the identity key"}}},"description":"Leads to add. Keys other than `email` must match the campaign's lead schema."}}}}},"required":true},"parameters":[{"schema":{"type":"string"},"in":"path","name":"campaign_id","required":true,"description":"Campaign ID"}],"security":[{"apiKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"message":{"type":"string"},"data":{"type":"object","properties":{"added":{"type":"integer","description":"Leads newly created"},"updated":{"type":"integer","description":"Existing leads updated"},"skipped":{"type":"integer","description":"Rows skipped for a missing or non-string email — the rest of the batch still went in"},"dropped_fields":{"type":"array","items":{"type":"string"},"description":"Field names not in the campaign's lead schema, which were ignored"}}}}},"example":{"status":201,"message":"Leads added","data":{"added":2,"updated":1,"skipped":0,"dropped_fields":[]}}}}},"400":{"description":"No valid leads in the request","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"No valid leads in the request"}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Campaign not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Campaign not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}},"get":{"operationId":"listCampaignLeads","summary":"List Leads","tags":["Campaigns"],"description":"Returns a campaign's leads, newest first. Filter by `status` (e.g. `replied`, `in_sequence`) or `search` an email substring.\n\nNote that `added` means \"not yet enrolled\", which in practice only happens while the campaign has no published sequence — leads added to a paused campaign are already `in_sequence` with their next step held.","parameters":[{"schema":{"type":"string"},"in":"query","name":"page_no","required":false,"description":"1-indexed page number (default: 1)"},{"schema":{"type":"string"},"in":"query","name":"page_size","required":false,"description":"Leads per page (default: 20, max: 200)"},{"schema":{"type":"string","enum":["added","in_sequence","completed","removed","replied","responded","unsubscribed","suppressed","bounced"]},"in":"query","name":"status","required":false,"description":"Filter by lead status"},{"schema":{"type":"string"},"in":"query","name":"search","required":false,"description":"Case-insensitive email substring match"},{"schema":{"type":"string"},"in":"path","name":"campaign_id","required":true,"description":"Campaign ID"}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"type":"object","properties":{"leads":{"type":"array","items":{"$ref":"#/components/schemas/CampaignLead"}},"total_count":{"type":"integer"},"page_no":{"type":"integer"},"page_size":{"type":"integer"}}}}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Campaign not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Campaign not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}},"delete":{"operationId":"deleteCampaignLeads","summary":"Delete Leads","tags":["Campaigns"],"description":"Permanently deletes leads and their step and reply history. **This cannot be undone.**\n\nDeletion leaves no memory behind: re-adding the same address later enters the campaign fresh. To block an address for good, add it to the suppression list instead.\n\nUp to 500 leads per call. `missing` counts ids that were already gone — a foreign id, or an earlier partial run — so a healing retry that deletes nothing new doesn't read as a failure. If some deletes fail the response is a 500 carrying the counts; retry the same ids to finish the job.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["lead_ids"],"properties":{"lead_ids":{"type":"array","minItems":1,"maxItems":500,"items":{"type":"string"},"description":"Lead ids to delete permanently — from List Leads."}}}}},"required":true},"parameters":[{"schema":{"type":"string"},"in":"path","name":"campaign_id","required":true,"description":"Campaign ID"}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"message":{"type":"string"},"data":{"type":"object","properties":{"deleted":{"type":"integer","description":"Leads deleted by this request"},"missing":{"type":"integer","description":"Ids that were already gone — unknown to this campaign, or deleted by an earlier run"}}}}}}}},"400":{"description":"No lead ids given, or more than 500","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"No lead ids given, or more than 500"}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Campaign not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Campaign not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}},"500":{"description":"Some deletes failed — the rest are committed; retry the same ids to finish","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Some deletes failed — the rest are committed; retry the same ids to finish"}}}}}}},"/api/v1/campaigns/{campaign_id}/leads/{lead_id}":{"get":{"operationId":"getCampaignLead","summary":"Get Lead","tags":["Campaigns"],"description":"Returns one lead with its ordered step timeline — the per-step record of what was sent, when it completed, and whether it was opened or replied to.","parameters":[{"schema":{"type":"string"},"in":"path","name":"campaign_id","required":true,"description":"Campaign ID"},{"schema":{"type":"string"},"in":"path","name":"lead_id","required":true,"description":"Lead ID"}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"name":{"type":"string","nullable":true},"status":{"type":"string"},"email_provider":{"type":"string","nullable":true,"description":"Mail provider derived from the address's MX records"},"fields":{"type":"object","additionalProperties":true,"description":"Custom field values, keyed by field name from the campaign's `lead_schema`"},"emails_sent":{"type":"integer"},"replied":{"type":"boolean"},"positive":{"type":"boolean","description":"Whether the reply was positive. **Not populated yet** — no reply classifier exists, so this is always false. Do not build on it until reply classification ships."},"current_node_id":{"type":"string","nullable":true},"next_run_at":{"type":"string","nullable":true,"format":"date-time"},"replied_at":{"type":"string","nullable":true,"format":"date-time"},"created_at":{"type":"string","nullable":true,"format":"date-time"},"steps":{"type":"array","description":"Ordered step timeline","items":{"type":"object","properties":{"node_id":{"type":"string","nullable":true},"node_type":{"type":"string","nullable":true},"status":{"type":"string","nullable":true,"description":"entered | waiting | sending | sent | failed | skipped | held | done"},"hold_reason":{"type":"string","nullable":true,"description":"Why a `held` step is waiting (e.g. `campaign_paused`). A hold is not a failure — `error` stays null."},"error":{"type":"string","nullable":true,"description":"Failure detail, when the step actually failed"},"started_at":{"type":"string","nullable":true,"format":"date-time"},"completed_at":{"type":"string","nullable":true,"format":"date-time"},"next_run_at":{"type":"string","nullable":true,"format":"date-time"},"opened_at":{"type":"string","nullable":true,"format":"date-time"},"open_count":{"type":"integer"},"replied_at":{"type":"string","nullable":true,"format":"date-time"}}}}}}}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Campaign or lead not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Campaign or lead not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/campaigns/{campaign_id}/metrics":{"get":{"operationId":"getCampaignMetrics","summary":"Get Campaign Metrics","tags":["Campaigns"],"description":"Returns aggregate campaign performance — leads, emails sent, opens, replies, bounces and their rates — plus a send trend bucketed over time. Use `days` (or `from`/`to`) and `granularity` to shape the trend window.","parameters":[{"schema":{"type":"string","enum":["hour","day"]},"in":"query","name":"granularity","required":false,"description":"Trend bucket size. Defaults to `day`."},{"schema":{"type":"string"},"in":"query","name":"tz","required":false,"description":"IANA timezone for bucketing (e.g. `America/New_York`)"},{"schema":{"type":"string"},"in":"query","name":"days","required":false,"description":"Trailing window in days"},{"schema":{"type":"string"},"in":"query","name":"from","required":false,"description":"Window start (ISO 8601)"},{"schema":{"type":"string"},"in":"query","name":"to","required":false,"description":"Window end (ISO 8601)"},{"schema":{"type":"string"},"in":"path","name":"campaign_id","required":true,"description":"Campaign ID"}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/CampaignMetrics"}}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Campaign not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Campaign not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/campaigns/":{"post":{"operationId":"createCampaign","summary":"Create Campaign","tags":["Campaigns"],"description":"Creates an empty campaign. It starts **paused** with no sequence, so nothing sends until you add steps, publish the sequence, choose the mailboxes to send from, add leads, and resume it.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["name"],"additionalProperties":false,"properties":{"name":{"type":"string","minLength":1,"description":"Campaign name","example":"Q3 Outbound"}}}}},"required":true},"security":[{"apiKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"message":{"type":"string"},"data":{"$ref":"#/components/schemas/Campaign"}}}}}},"400":{"description":"Campaign name is required","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Campaign name is required"}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}},"get":{"operationId":"listCampaigns","summary":"List Campaigns","tags":["Campaigns"],"description":"Returns campaigns you own or that are shared with you. Pass `scope=org` to list every campaign in your organization. Archived campaigns are excluded.","parameters":[{"schema":{"type":"string","enum":["mine","org"]},"in":"query","name":"scope","required":false,"description":"`mine` (default) — owned or shared with you; `org` — all campaigns in the organization"}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Campaign"}}}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/campaigns/{campaign_id}":{"get":{"operationId":"getCampaign","summary":"Get Campaign","tags":["Campaigns"],"description":"Returns a single campaign including `lead_schema` — the fields to use as keys when adding leads. Register new fields with Update Lead Schema.\n\nA campaign that is deleted, never existed, or belongs to another organization all return 404 alike.","parameters":[{"schema":{"type":"string"},"in":"path","name":"campaign_id","required":true,"description":"Campaign ID"}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/Campaign"}}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Campaign not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Campaign not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}},"delete":{"operationId":"deleteCampaign","summary":"Delete Campaign","tags":["Campaigns"],"description":"Deletes a campaign permanently: it stops sending and every campaign endpoint then reports it as 404, the same as one that never existed. There is no restore, and a second delete returns 404. Its leads, steps and history are retained internally for reporting but are no longer reachable through the API.\n\nNo request body.","parameters":[{"schema":{"type":"string"},"in":"path","name":"campaign_id","required":true,"description":"Campaign ID"}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"message":{"type":"string"},"data":{"type":"object","properties":{"campaign_id":{"type":"string"},"deleted":{"type":"boolean"}}}}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Campaign not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Campaign not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/campaigns/{campaign_id}/mailboxes":{"get":{"operationId":"getCampaignMailboxes","summary":"Get Campaign Mailboxes","tags":["Campaigns"],"description":"Returns the campaign's sender selection plus everything needed to change it: every mailbox in the organization flagged `selected`, the teammates currently pooled, and the org members available to pool.\n\nSends round-robin across the resolved pool, respecting each mailbox's daily cap. Connect more mailboxes in the Floqer UI under Connections → Mailboxes.","parameters":[{"schema":{"type":"string"},"in":"path","name":"campaign_id","required":true,"description":"Campaign ID"}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"type":"object","properties":{"mode":{"type":"string","enum":["team_members","specific_mailboxes","none"],"description":"`team_members` pools every mailbox owned by the selected teammates (including ones they connect later); `specific_mailboxes` uses exactly the selected mailboxes; `none` means no sender is selected yet and the campaign cannot send."},"mailboxes":{"type":"array","items":{"$ref":"#/components/schemas/CampaignMailbox"}},"member_emails":{"type":"array","items":{"type":"string"},"description":"Teammates whose mailboxes are pooled."},"available_members":{"type":"array","description":"Org members that can be pooled, for the team-member picker.","items":{"type":"object","properties":{"email":{"type":"string"},"name":{"type":"string"},"title":{"type":"string"}}}}}}}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Campaign not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Campaign not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/campaigns/{campaign_id}/settings":{"patch":{"operationId":"updateCampaignSettings","summary":"Update Campaign Settings","tags":["Campaigns"],"description":"Updates a campaign's sending window, reply behaviour, bounce protection, and the mailboxes it sends from.\n\n**Only what you pass changes** — omitted keys are left alone, and `null` clears a key back to its default. Omit `send_from` to leave the sender selection untouched.\n\n`send_from.mode` picks one sender strategy and clears the other: `team_members` pools every mailbox owned by the named teammates (including any they connect later, sends spread evenly across the team), while `specific_mailboxes` uses exactly the named mailboxes. Ids and emails that aren't live in your organization are dropped rather than rejected.\n\n**Changing `send_from` requires the campaign to be paused** — the pool re-resolves on every send, so a mid-flight change would reroute in-sequence leads. Everything else can be changed while the campaign is running.\n\nTo set the priority of tasks created on reply, patch the answering send step's `config.replyTaskPriority` — it is per-step, not a campaign setting.\n\nRead current values from `settings` on Get Campaign and from Get Campaign Mailboxes.","requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"sending_window_start":{"type":"string","nullable":true,"pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Earliest send time as `HH:MM` (24h, campaign timezone). `null` sends anytime — leave both window keys null for no restriction.","example":"09:00"},"sending_window_end":{"type":"string","nullable":true,"pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Latest send time as `HH:MM` (24h, campaign timezone). `null` sends anytime.","example":"17:30"},"timezone":{"type":"string","maxLength":64,"description":"IANA timezone the sending window is measured in.","example":"America/New_York"},"remove_on_reply":{"type":"boolean","description":"Stop a lead's sequence as soon as they reply. Defaults to true."},"create_task_on_reply":{"type":"boolean","description":"Create a task when a lead replies. Defaults to true."},"bounce_rate_limit":{"type":"number","nullable":true,"description":"Auto-pause the campaign when the share of contacted leads whose emails bounce reaches this percentage. `0` turns protection off; otherwise 1–5. Lower is stricter and protects sender reputation sooner. `null` restores the default (5).","example":3},"send_from":{"type":"object","required":["mode"],"additionalProperties":false,"description":"Who the campaign sends from. Requires the campaign to be paused. Omit to leave unchanged.","properties":{"mode":{"type":"string","enum":["team_members","specific_mailboxes"],"description":"Which sender strategy to use. The other selection is cleared."},"member_emails":{"type":"array","items":{"type":"string"},"description":"Teammates to pool. Required (non-empty) when `mode` is `team_members`."},"mailbox_ids":{"type":"array","items":{"type":"string"},"description":"Mailboxes to send from — ids from Get Campaign Mailboxes. Required (non-empty) when `mode` is `specific_mailboxes`."}}}}}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"campaign_id","required":true,"description":"Campaign ID"}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"message":{"type":"string"},"data":{"type":"object","properties":{"campaign_id":{"type":"string"},"settings":{"type":"object","additionalProperties":true,"description":"The merged settings"},"send_from":{"type":"object","description":"The campaign's sender selection after the update","properties":{"mode":{"type":"string","enum":["team_members","specific_mailboxes","none"]},"mailbox_ids":{"type":"array","items":{"type":"string"}},"member_emails":{"type":"array","items":{"type":"string"}}}}}}}}}}},"400":{"description":"Unknown or invalid settings key, or an empty sender selection","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unknown or invalid settings key, or an empty sender selection"}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Campaign not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Campaign not found"}}}},"409":{"description":"Campaign is active — pause it before changing `send_from`","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Campaign is active — pause it before changing `send_from`"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/campaigns/{campaign_id}/pause":{"post":{"operationId":"pauseCampaign","summary":"Pause Campaign","tags":["Campaigns"],"description":"Pauses a campaign — every lead's next step holds until the campaign is resumed. In-flight leads are held, not cancelled.","parameters":[{"schema":{"type":"string"},"in":"path","name":"campaign_id","required":true,"description":"Campaign ID"}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"message":{"type":"string"},"data":{"type":"object","properties":{"campaign_id":{"type":"string"},"state":{"type":"string","enum":["paused"]}}}}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Campaign not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Campaign not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/campaigns/{campaign_id}/resume":{"post":{"operationId":"resumeCampaign","summary":"Resume Campaign","tags":["Campaigns"],"description":"Resumes a paused campaign and restarts its held leads. Also enrols leads that were added before a sequence existed. A campaign with no published sequence resumes but enrols nobody — publish first.\n\nSafe to call on a campaign that is already active: it skips the hold sweep and reports `resumed: 0, requeued: 0`, still enrolling any leads waiting at `added`. The counters therefore only ever describe what this call actually changed.","parameters":[{"schema":{"type":"string"},"in":"path","name":"campaign_id","required":true,"description":"Campaign ID"}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"message":{"type":"string"},"data":{"type":"object","properties":{"campaign_id":{"type":"string"},"state":{"type":"string","enum":["active"]},"resumed":{"type":"integer","description":"Leads whose campaign-pause hold this call released. `0` when the campaign was already running and nothing was held."},"requeued":{"type":"integer","description":"Held leads re-queued to advance. Re-queuing is idempotent, so this can be non-zero on a repeat call while `resumed` is 0."},"enrolled":{"type":"integer","description":"Leads enrolled into the sequence by this call"}}}}}}}},"401":{"description":"Unauthorized — missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Unauthorized — missing or invalid API key"}}}},"403":{"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Forbidden. `code` says which permission refused: ROLE_FORBIDDEN (your role), INSUFFICIENT_SCOPE (the API key's scopes), WORKFLOW_FORBIDDEN (no access to the workflow), KEY_WORKFLOW_FORBIDDEN (the API key's workflow restriction)."}}}},"404":{"description":"Campaign not found","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Campaign not found"}}}},"429":{"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets.","headers":{"X-RateLimit-Limit":{"schema":{"schema":{"type":"integer","examples":[60]}}},"X-RateLimit-Remaining":{"schema":{"schema":{"type":"integer","examples":[57]}}},"X-RateLimit-Reset":{"schema":{"schema":{"type":"integer","examples":[1710345600]}}},"Retry-After":{"schema":{"schema":{"type":"integer","examples":[30]}}}},"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}},"description":"Too Many Requests — rate limit exceeded. Check `Retry-After` header for seconds until the limit resets."}}}}}}},"/api/v1/ackdb/syncs":{"get":{"operationId":"listAckdbSyncs","summary":"List ackDB Syncs","tags":["ackDB"],"description":"List the organization's durable ackDB segment-to-Floq syncs, newest first. This stored-state list does not call ackDB for a live segment count; use Get Sync for that detail. A key restricted by `allowed_workflow_ids` sees only permitted destinations.","security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbSync"}}},"required":["status","data"]}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}}}}},"/api/v1/ackdb/segments/{segment_id}/sync":{"post":{"operationId":"createAckdbSync","summary":"Sync ackDB Segment to Floq","tags":["ackDB"],"description":"Create a durable sync from an ackDB segment into a published Floq workflow. Every enter, exit, and re-entry is appended as a new workflow row with `Segment Change` set to `added` or `removed`. `field_mapping` keys are public workflow input references from List Inputs; values are ackDB segment field names. Existing members are optional synthetic `added` events. By default rows are inserted without running the workflow.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbCreateSyncBody"}}}},"parameters":[{"schema":{"type":"string","minLength":1},"in":"path","name":"segment_id","required":true}],"security":[{"apiKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"type":"object","properties":{"sync_id":{"type":"string"},"connection_id":{"type":"string"},"segment_id":{"type":"string"},"workflow_id":{"type":"string","format":"uuid"},"entity_kind":{"type":"string","enum":["company","contact","person"]},"state":{"type":"string","enum":["seeding","active","paused","pending_confirmation"]},"reused_sync":{"type":"boolean"}},"required":["sync_id","connection_id","segment_id","workflow_id","entity_kind","state","reused_sync"]}},"required":["status","data"]}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}},"502":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}}}},"get":{"operationId":"getAckdbSync","summary":"Get ackDB Sync","tags":["ackDB"],"description":"Read the durable sync for one ackDB segment, including destination connections, delivery counters, current lifecycle state, breaker/error details, and a best-effort live segment size. An unsynced or inaccessible segment returns `synced: false`.","parameters":[{"schema":{"type":"string","minLength":1},"in":"path","name":"segment_id","required":true}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"$ref":"#/components/schemas/AckdbSync"}},"required":["status","data"]}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}}}},"delete":{"operationId":"deleteAckdbSync","summary":"Delete ackDB Sync","tags":["ackDB"],"description":"Stop the segment sync, archive every destination connection, and remove its scheduler. Rows already delivered to workflows remain. A workflow-restricted key must cover every destination.","parameters":[{"schema":{"type":"string","minLength":1},"in":"path","name":"segment_id","required":true}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"type":"object","properties":{"sync_id":{"type":"string"},"connections_archived":{"type":"integer"}},"required":["sync_id","connections_archived"]}},"required":["status","data"]}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}}}}},"/api/v1/ackdb/segments/{segment_id}/sync/confirm":{"post":{"operationId":"confirmAckdbSync","summary":"Confirm Held ackDB Sync Members","tags":["ackDB"],"description":"Resolve a sync breaker in `pending_confirmation`: `push` releases held members for delivery, while `skip` records them as skipped. A workflow-restricted key must cover every destination on the sync.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbConfirmSyncBody"}}}},"parameters":[{"schema":{"type":"string","minLength":1},"in":"path","name":"segment_id","required":true}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"type":"object","properties":{"sync_id":{"type":"string"},"action":{"type":"string","enum":["push","skip"]},"affected":{"type":"integer"}},"required":["sync_id","action","affected"]}},"required":["status","data"]}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}}}}},"/api/v1/ackdb/segments/{segment_id}/sync/resume":{"post":{"operationId":"resumeAckdbSync","summary":"Resume ackDB Sync","tags":["ackDB"],"description":"Resume a sync watcher that auto-paused after an import error. This clears the surfaced error and restarts its scheduler. Breaker-held syncs use Confirm instead. A workflow-restricted key must cover every destination.","parameters":[{"schema":{"type":"string","minLength":1},"in":"path","name":"segment_id","required":true}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"type":"object","properties":{"sync_id":{"type":"string"},"state":{"type":"string","enum":["active"]}},"required":["sync_id","state"]}},"required":["status","data"]}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}}}}},"/api/v1/ackdb/sync/connections/{connection_id}":{"patch":{"operationId":"patchAckdbSyncConnection","summary":"Update ackDB Sync Destination","tags":["ackDB"],"description":"Update one sync destination. `field_mapping` replaces the mapping for future deliveries; existing workflow rows are not re-pushed. `status: paused` holds new changes until Resume chooses how to deliver them. `reentry_mode` applies only to legacy current-state syncs; event ledgers always append re-entry as `added`.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbPatchSyncConnectionBody"}}}},"parameters":[{"schema":{"type":"string","pattern":"^[1-9][0-9]*$"},"in":"path","name":"connection_id","required":true}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"type":"object","properties":{"connection_id":{"type":"string"},"status":{"type":["null","string"],"enum":["paused",null]},"reentry_mode":{"type":["null","string"],"enum":["suppress","repush",null]},"remapped":{"type":"boolean"}},"required":["connection_id","status","reentry_mode","remapped"]}},"required":["status","data"]}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}}}},"delete":{"operationId":"deleteAckdbSyncConnection","summary":"Delete ackDB Sync Destination","tags":["ackDB"],"description":"Archive one destination connection. Other workflows on the same segment sync continue receiving members, and rows already delivered to this workflow remain.","parameters":[{"schema":{"type":"string","pattern":"^[1-9][0-9]*$"},"in":"path","name":"connection_id","required":true}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"type":"object","properties":{"connection_id":{"type":"string"}},"required":["connection_id"]}},"required":["status","data"]}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}}}}},"/api/v1/ackdb/sync/connections/{connection_id}/resume":{"post":{"operationId":"resumeAckdbSyncConnection","summary":"Resume ackDB Sync Destination","tags":["ackDB"],"description":"Resume a paused destination and choose what happens to members accumulated while paused: `deliver` queues them, while `skip` records them as skipped. Confirm any pending whole-sync changes before resuming a destination.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbResumeSyncConnectionBody"}}}},"parameters":[{"schema":{"type":"string","pattern":"^[1-9][0-9]*$"},"in":"path","name":"connection_id","required":true}],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"integer"},"data":{"type":"object","properties":{"connection_id":{"type":"string"},"backlog":{"type":"string","enum":["deliver","skip"]},"affected":{"type":"integer"}},"required":["connection_id","backlog","affected"]}},"required":["status","data"]}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSyncErrorResponse"}}}}}}},"/api/v1/ackdb/schemas/sources":{"get":{"tags":["ackDB"],"summary":"List all source types","description":"Returns all registered source definitions (global defaults). Each source defines a content schema, identity link types, and its event contract.\n\nOn a fresh deployment the four person-first packs (job, education, web, signup) are registered ENABLED, plus the internal only `system` source (#453). The six legacy packs (stripe, slack, email, meetings, crm, calendar) are no longer seeded; a deployment already running one keeps it. A source registered with `disabled: true` is discoverable here but rejects new ingest (422 `SOURCE_DISABLED`) until an operator turns it on with `PUT /schemas/sources/{sourceType} { \"disabled\": false }`.","operationId":"listSources","responses":{"200":{"description":"List of source definitions","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbSourceDefinition"}}},"required":["data"]},"example":{"data":[{"sourceType":"job","contentSchema":{"event_type":"string","stint_id":"string","full_name":"string","linkedin_url":"string","company":"string","started_at":"datetime","title":"string?","title_category":"string?","decision_maker_status":"boolean?","location":"string?","ended_at":"datetime?"},"identityLinkTypes":["domain"],"defaultTraits":[],"validEventTypes":["role_started","role_ended"],"eventTypeField":"event_type","eventTypePrefix":"job_","eventMetadataFields":["stint_id","full_name","linkedin_url","company","started_at","title","title_category","decision_maker_status","location","ended_at"],"createdAt":"2026-07-01T00:00:00.000Z"}]}}}}},"security":[{"apiKey":[]}]},"post":{"tags":["ackDB"],"summary":"Declare a custom source","description":"Declares a new custom source definition at runtime. A source declared here is INDISTINGUISHABLE from a shipped pack: it flows through the same DB tables, so ingest, discovery, agent guidance, contact extraction, and embedding all treat it identically.\n\n`eventTypeField` is REQUIRED (no static single-event sources) and `identityLinkTypes` MUST include `domain` so the source can resolve to or create entities. Default trait keys MUST be namespaced as `<sourceType>_*`. Use `?dryRun=true` to validate the payload without persisting anything.","operationId":"createSource","parameters":[{"name":"dryRun","in":"query","required":false,"description":"Validate without persisting. Returns 200 with `{ valid: true, dryRun: true, ... }`.","schema":{"type":"boolean"},"example":true}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSourceCreateRequest"},"example":{"sourceType":"app_usage","displayName":"App Usage","description":"Daily product-usage rollups and per-user login activity from our application.","version":"1.0.0","queryingGuide":"Use usage.daily_rollup for trend/aggregate questions (DAU, credits burned). Use user.login for per-person engagement and seat activity.","identityLinkTypes":["domain","app_account_id"],"eventTypeField":"event_type","eventTypePrefix":"app_usage_","defaultContentNature":"structured","allowCustomEventTypes":false,"defaultTraits":[{"traitKey":"app_usage_dau","valueType":"number","description":"Most recent daily active users for the account."},{"traitKey":"app_usage_credits_total","valueType":"number","description":"Lifetime credits consumed by the account."}],"events":[{"eventType":"usage.daily_rollup","description":"One row per account per day summarizing product usage.","category":"usage","contentNature":"structured","eventScope":"entity","fields":{"account_id":{"type":"string","required":true,"description":"Application account identifier.","computeHint":"Map from the rollup row's `account_id`.","example":"acct_123"},"date":{"type":"datetime","required":true,"description":"The UTC day this rollup covers.","computeHint":"Map from the rollup row's `date` (start of day, UTC).","example":"2026-06-15T00:00:00Z"},"sessions":{"type":"number","required":true,"description":"Number of sessions on this day.","computeHint":"Map from `metrics.sessions`.","example":84},"credits_consumed":{"type":"number","required":true,"description":"Credits consumed on this day.","computeHint":"Map from `metrics.credits_consumed`.","example":1200}},"promotedMetadataFields":["account_id","sessions","credits_consumed"],"ingestExample":{"source":"app_usage","content":{"text":"Acme used 1200 credits across 84 sessions on 2026-06-15.","metadata":{"event_type":"usage.daily_rollup","account_id":"acct_123","date":"2026-06-15T00:00:00Z","sessions":84,"credits_consumed":1200}},"identityLinks":[{"externalIdType":"domain","externalIdValue":"acme.com","confidence":1},{"externalIdType":"app_account_id","externalIdValue":"acct_123","confidence":1}],"traits":[{"traitKey":"app_usage_dau","value":84,"workflow":"app-usage-rollup","confidence":1}],"timestamp":"2026-06-15T00:00:00Z"}},{"eventType":"user.login","description":"A single user logged in to the application.","category":"usage","contentNature":"activity-signal","eventScope":"person","fields":{"account_id":{"type":"string","required":true,"description":"Application account the user belongs to.","computeHint":"Map from the login event's `account_id`.","example":"acct_123"},"user_email":{"type":"string","required":true,"description":"Email of the user who logged in.","computeHint":"Map from `user.email`.","example":"jane@acme.com"},"user_name":{"type":"string","required":false,"description":"Display name of the user who logged in.","computeHint":"Map from `user.name`.","example":"Jane Doe"}},"promotedMetadataFields":["account_id","user_email"],"contactMapping":{"role":"user","nameField":"user_name","emailField":"user_email","externalIdField":"user_email","externalIdType":"app_user_email"},"ingestExample":{"source":"app_usage","content":{"text":"Jane Doe logged in.","metadata":{"event_type":"user.login","account_id":"acct_123","user_email":"jane@acme.com","user_name":"Jane Doe"}},"identityLinks":[{"externalIdType":"domain","externalIdValue":"acme.com","confidence":1},{"externalIdType":"app_account_id","externalIdValue":"acct_123","confidence":1}],"traits":[],"timestamp":"2026-06-15T09:30:00Z"}}]}}}},"responses":{"200":{"description":"Dry-run validation succeeded (nothing persisted)","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"valid":{"type":"boolean","enum":[true]},"dryRun":{"type":"boolean","enum":[true]},"sourceType":{"type":"string"},"eventTypes":{"type":"array","items":{"type":"string"}}},"required":["valid","dryRun","sourceType","eventTypes"]}},"required":["data"]},"example":{"data":{"valid":true,"dryRun":true,"sourceType":"app_usage","eventTypes":["usage.daily_rollup","user.login"]}}}}},"201":{"description":"Source created, event types and default traits registered","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"source":{"$ref":"#/components/schemas/AckdbSourceDefinition"},"eventTypesCreated":{"type":"integer","description":"Number of event types created"},"traitsCreated":{"type":"integer","description":"Number of new default trait definitions created"}},"required":["source","eventTypesCreated","traitsCreated"]}},"required":["data"]}}}},"400":{"description":"Validation failed (bad shape, missing `domain` link, non-namespaced trait, etc.)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"},"example":{"error":{"code":"VALIDATION_ERROR","message":"identityLinkTypes must include \"domain\""}}}}},"409":{"description":"A source with this `sourceType` already exists (use PUT to edit)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"},"example":{"error":{"code":"SOURCE_EXISTS","message":"Source \"app_usage\" already exists. Use PUT to edit."}}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/schemas/sources/{sourceType}":{"get":{"tags":["ackDB"],"summary":"Get source definition","description":"Returns the full definition for a source type, including content schema, identity link types, and default traits.","operationId":"getSource","parameters":[{"name":"sourceType","in":"path","required":true,"description":"Source type identifier (e.g. `slack`, `stripe`)","schema":{"type":"string"},"examples":{"slack":{"value":"slack"},"stripe":{"value":"stripe"}}}],"responses":{"200":{"description":"Source definition found","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbSourceDefinition"}},"required":["data"]}}}},"404":{"description":"Source type not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"},"example":{"error":{"code":"SOURCE_NOT_FOUND","message":"Source type not found: unknown"}}}}}},"security":[{"apiKey":[]}]},"put":{"tags":["ackDB"],"summary":"Edit a source (custom OR shipped pack)","description":"Partial update of a source definition. Every field is optional; provided fields replace the existing ones. Editing a shipped pack IS allowed - the edit marks the row `user_modified` so the boot loader stops overwriting it. `identityLinkTypes`, if provided, MUST still include `domain`. Set `disabled: true` to stop the source accepting new ingest without deleting it.","operationId":"updateSource","parameters":[{"name":"sourceType","in":"path","required":true,"description":"Source type identifier (e.g. `app_usage`, `slack`)","schema":{"type":"string"},"example":"app_usage"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSourceUpdateRequest"},"example":{"displayName":"App Usage (Production)","queryingGuide":"Use usage.daily_rollup for trend questions, user.login for per-seat engagement.","identityLinkTypes":["domain","app_account_id"],"disabled":false}}}},"responses":{"200":{"description":"Source updated","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"source":{"$ref":"#/components/schemas/AckdbSourceDefinition"},"traitsCreated":{"type":"integer","description":"Number of new default trait definitions created by this edit"}},"required":["source","traitsCreated"]}},"required":["data"]}}}},"400":{"description":"Validation failed (e.g. `identityLinkTypes` no longer includes `domain`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"},"example":{"error":{"code":"VALIDATION_ERROR","message":"identityLinkTypes must include \"domain\""}}}}},"404":{"description":"Source type not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"},"example":{"error":{"code":"SOURCE_NOT_FOUND","message":"Source type not found: app_usage"}}}}}},"security":[{"apiKey":[]}]},"patch":{"tags":["ackDB"],"summary":"Safely edit a source (guards data-orphaning removals)","description":"The UI-safe counterpart to PUT at the source level. PUT already merges by omission here, so what PATCH adds is the guard set: removing an identity link type that still has stored `identity_links` rows, or a default trait that still holds values, is REFUSED (either would orphan data and leave a dangling Observe column). Changing `eventTypeField`/`eventTypePrefix` is refused once the source has data (stored events would stop matching their definitions). PATCH never touches the `disabled` lifecycle flag — pause/resume stays on PUT. Editing marks the row `user_modified`.","operationId":"patchSource","parameters":[{"name":"sourceType","in":"path","required":true,"description":"Source type identifier (e.g. `app_usage`, `slack`)","schema":{"type":"string"},"example":"app_usage"}],"requestBody":{"required":true,"description":"A partial source. At least one key required. Omitted keys are preserved.","content":{"application/json":{"schema":{"type":"object"},"example":{"displayName":"App Usage (Production)","queryingGuide":"Use usage.daily_rollup for trend questions, user.login for per-seat engagement."}}}},"responses":{"200":{"description":"Source updated; omitted keys preserved","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"source":{"$ref":"#/components/schemas/AckdbSourceDefinition"},"traitsCreated":{"type":"integer"}}}}}}}},"400":{"description":"Validation failed, or a removal was refused because it would orphan stored data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"},"example":{"error":{"code":"VALIDATION_ERROR","message":"Cannot remove default trait \"app_usage_total_sessions\" from \"app_usage\" — it holds stored values."}}}}},"404":{"description":"Source not found"},"409":{"description":"An identityLinkType clashes with a universal link type"}},"security":[{"apiKey":[]}]},"delete":{"tags":["ackDB"],"summary":"Delete a custom source","description":"Deletes a custom source and its event types. Shipped packs cannot be deleted - disable them via PUT instead. A source that has ingested rows cannot be deleted either; purge its data first via `DELETE /data/purge?source=<sourceType>`.","operationId":"deleteSource","parameters":[{"name":"sourceType","in":"path","required":true,"description":"Custom source type identifier (e.g. `app_usage`)","schema":{"type":"string"},"example":"app_usage"}],"responses":{"200":{"description":"Source deleted","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"deleted":{"type":"boolean","enum":[true]},"sourceType":{"type":"string"}},"required":["deleted","sourceType"]}},"required":["data"]},"example":{"data":{"deleted":true,"sourceType":"app_usage"}}}}},"404":{"description":"Source type not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"},"example":{"error":{"code":"SOURCE_NOT_FOUND","message":"Source type not found: app_usage"}}}}},"409":{"description":"Source cannot be deleted: it is not disabled (disable first) or it has ingested data (purge first). Shipped packs CAN be deleted (#320); the deletion is tombstoned so boot does not re-seed them.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"},"examples":{"notDisabled":{"summary":"Source is not disabled","value":{"error":{"code":"SOURCE_NOT_DISABLED","message":"\"app_usage\" must be disabled before it can be deleted. PUT /schemas/sources/app_usage { \"disabled\": true } first."}}},"hasData":{"summary":"Source has ingested data","value":{"error":{"code":"SOURCE_HAS_DATA","message":"\"app_usage\" has ingested data and cannot be deleted. Purge it first (DELETE /data/purge?source=app_usage)."}}}}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/schemas/sources/{sourceType}/events":{"get":{"tags":["ackDB"],"summary":"List event types for a source","description":"Returns all event type definitions for a source, with rich field descriptions, compute hints, examples, and complete ingest examples. This is the primary endpoint for workflow tool builders to discover what data ackDB expects for each event type. Each event type has a per-event content schema (different from the flat source-level schema).","operationId":"listEventTypes","parameters":[{"name":"sourceType","in":"path","required":true,"description":"Source type (e.g. `stripe`, `slack`)","schema":{"type":"string"},"example":"stripe"}],"responses":{"200":{"description":"List of event type definitions with full field documentation","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbEventTypeDefinition"}},"meta":{"type":"object","properties":{"sourceType":{"type":"string"},"displayName":{"type":"string"},"description":{"type":"string"},"version":{"type":"string"},"totalEventTypes":{"type":"integer"}}}},"required":["data"]}}}},"404":{"description":"Source type not found"}},"security":[{"apiKey":[]}]},"post":{"tags":["ackDB"],"summary":"Add an event type to a source (custom OR pack)","description":"Adds a new event type to an existing source. Works on custom sources and shipped packs alike - adding an event type to a pack marks that event row `user_modified` so the boot loader stops overwriting it. 409 if the event type already exists (use PUT to edit it).","operationId":"createEventType","parameters":[{"name":"sourceType","in":"path","required":true,"description":"Source type the event belongs to (e.g. `app_usage`)","schema":{"type":"string"},"example":"app_usage"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbEventTypeInput"},"example":{"eventType":"usage.daily_rollup","description":"One row per account per day summarizing product usage.","category":"usage","contentNature":"structured","eventScope":"entity","fields":{"account_id":{"type":"string","required":true,"description":"Application account identifier.","computeHint":"Map from the rollup row's `account_id`.","example":"acct_123"},"date":{"type":"datetime","required":true,"description":"The UTC day this rollup covers.","computeHint":"Map from the rollup row's `date` (start of day, UTC).","example":"2026-06-15T00:00:00Z"},"sessions":{"type":"number","required":true,"description":"Number of sessions on this day.","computeHint":"Map from `metrics.sessions`.","example":84},"credits_consumed":{"type":"number","required":true,"description":"Credits consumed on this day.","computeHint":"Map from `metrics.credits_consumed`.","example":1200}},"promotedMetadataFields":["account_id","sessions","credits_consumed"]}}}},"responses":{"201":{"description":"Event type created","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbEventTypeDefinition"}},"required":["data"]}}}},"400":{"description":"Validation failed (bad field shape, unknown requiredWhen reference, etc.)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"},"examples":{"missingFields":{"summary":"Missing required `fields` object","value":{"error":{"code":"VALIDATION_ERROR","message":"fields: Required"}}},"badRequiredWhen":{"summary":"requiredWhen points at a field that does not exist on the event","value":{"error":{"code":"VALIDATION_ERROR","message":"Event \"invitee.responded\" field \"no_show_detection_method\" requiredWhen references unknown field \"responsestatus\"."}}}}}}},"404":{"description":"Source type not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"},"example":{"error":{"code":"SOURCE_NOT_FOUND","message":"Source type not found: app_usage"}}}}},"409":{"description":"An event type with this name already exists for the source (use PUT to edit)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"},"example":{"error":{"code":"EVENT_TYPE_EXISTS","message":"Event type \"usage.daily_rollup\" already exists for \"app_usage\". Use PUT to edit."}}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/schemas/sources/{sourceType}/events/{eventType}":{"get":{"tags":["ackDB"],"summary":"Get a single event type definition","description":"Returns the full definition for a specific event type, including field descriptions, compute hints, examples, trait hints, and a complete POST /ingest example. Workflow tool builders can use the ingest example as a starting template.","operationId":"getEventType","parameters":[{"name":"sourceType","in":"path","required":true,"description":"Source type (e.g. `stripe`)","schema":{"type":"string"},"example":"stripe"},{"name":"eventType","in":"path","required":true,"description":"Event type (e.g. `invoice.paid`, `customer.subscription.created`)","schema":{"type":"string"},"example":"invoice.paid"}],"responses":{"200":{"description":"Event type definition with full field documentation and ingest example","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbEventTypeDefinition"}},"required":["data"]}}}},"404":{"description":"Event type not found"}},"security":[{"apiKey":[]}]},"put":{"tags":["ackDB"],"summary":"Edit an event type (additive only)","description":"Edits an existing event type. Changes are ADDITIVE only: an existing field's `type` cannot change and an optional field cannot become required - either would invalidate already-stored payloads. `eventType` in the body MUST match the path (event types cannot be renamed). Editing a pack-owned event marks the row `user_modified` so the boot loader stops overwriting it.","operationId":"updateEventType","parameters":[{"name":"sourceType","in":"path","required":true,"description":"Source type the event belongs to (e.g. `app_usage`)","schema":{"type":"string"},"example":"app_usage"},{"name":"eventType","in":"path","required":true,"description":"Event type to edit (e.g. `usage.daily_rollup`)","schema":{"type":"string"},"example":"usage.daily_rollup"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbEventTypeInput"},"example":{"eventType":"usage.daily_rollup","description":"One row per account per day summarizing product usage (now includes API calls).","category":"usage","contentNature":"structured","eventScope":"entity","fields":{"account_id":{"type":"string","required":true,"description":"Application account identifier.","computeHint":"Map from the rollup row's `account_id`.","example":"acct_123"},"date":{"type":"datetime","required":true,"description":"The UTC day this rollup covers.","computeHint":"Map from the rollup row's `date` (start of day, UTC).","example":"2026-06-15T00:00:00Z"},"sessions":{"type":"number","required":true,"description":"Number of sessions on this day.","computeHint":"Map from `metrics.sessions`.","example":84},"credits_consumed":{"type":"number","required":true,"description":"Credits consumed on this day.","computeHint":"Map from `metrics.credits_consumed`.","example":1200},"api_calls":{"type":"number","required":false,"description":"New optional field: API calls on this day.","computeHint":"Map from `metrics.api_calls`.","example":5300}},"promotedMetadataFields":["account_id","sessions","credits_consumed","api_calls"]}}}},"responses":{"200":{"description":"Event type updated","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbEventTypeDefinition"}},"required":["data"]}}}},"400":{"description":"Validation failed: a breaking field change (type changed, optional→required), or the body `eventType` does not match the path","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"},"examples":{"breakingChange":{"summary":"Breaking field change (type changed)","value":{"error":{"code":"VALIDATION_ERROR","message":"Field \"sessions\" type cannot change (\"number\" → \"string\") - breaks stored data."}}},"optionalToRequired":{"summary":"Optional field made required","value":{"error":{"code":"VALIDATION_ERROR","message":"Field \"api_calls\" cannot become required - would retroactively invalidate stored payloads."}}},"eventTypeMismatch":{"summary":"Body eventType does not match path","value":{"error":{"code":"VALIDATION_ERROR","message":"eventType in body (\"usage.weekly_rollup\") must match the path (\"usage.daily_rollup\"). Event types cannot be renamed."}}}}}}},"404":{"description":"Event type not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"},"example":{"error":{"code":"EVENT_TYPE_NOT_FOUND","message":"Event type not found: app_usage/usage.daily_rollup"}}}}}},"security":[{"apiKey":[]}]},"patch":{"tags":["ackDB"],"summary":"Safely edit an event type (non-destructive partial update)","description":"The UI-safe counterpart to PUT. Only the keys present in the body change; every other key — including the documentation payload a pack lives on (`computeHint`, `example`, `traitHints`, `ingestExample`, `category`, `rationale`, `replacesEvents`, and every contact mapping) — is preserved from the stored row. `fields` is a PARTIAL map merged attribute-by-attribute, so a description fix does not require resupplying a field's compute hint. Removing a field is never implied by omission — it requires an explicit `removeFields` array.\n\nThe additive-change guards from PUT still apply (a field's `type` cannot change; optional cannot become required). In addition, edits that would silently rewrite the meaning of already-stored data are REFUSED (not warned) while the event type holds data: adding a required field, flipping `eventScope`, removing a field or contact mapping, or flipping a mapping's `externalIdScope`. Unpromoting a field that a saved segment still references is always refused. Editing a pack-owned event marks the row `user_modified` so the boot loader stops overwriting it.","operationId":"patchEventType","parameters":[{"name":"sourceType","in":"path","required":true,"description":"Source type the event belongs to (e.g. `app_usage`)","schema":{"type":"string"},"example":"app_usage"},{"name":"eventType","in":"path","required":true,"description":"Event type to edit (e.g. `usage.daily_rollup`)","schema":{"type":"string"},"example":"usage.daily_rollup"}],"requestBody":{"required":true,"description":"A partial event. At least one key required. `fields` merges per-attribute; `removeFields` is the only way to drop a field. List keys (`promotedMetadataFields`, `contactMappings`, `traitHints`, `replacesEvents`) replace wholesale when present.","content":{"application/json":{"schema":{"type":"object"},"examples":{"fixDescription":{"summary":"Fix one field's description; everything else preserved","value":{"fields":{"sessions":{"description":"Distinct sessions on this day."}}}},"addOptionalField":{"summary":"Add an optional field","value":{"fields":{"api_calls":{"type":"number","required":false,"description":"API calls this day."}}}},"removeField":{"summary":"Explicitly remove a field (only allowed with no stored data)","value":{"removeFields":["deprecated_metric"]}}}}}},"responses":{"200":{"description":"Event type updated; unmentioned keys preserved","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbEventTypeDefinition"}},"required":["data"]}}}},"400":{"description":"Validation failed, or the edit was refused as destructive (required field added to a live event, eventScope flipped, field/mapping removed with data present, externalIdScope flipped, or a promoted field unpromoted while a saved segment uses it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"},"examples":{"destructiveRemoval":{"summary":"Field removal on a live event type","value":{"error":{"code":"VALIDATION_ERROR","message":"Cannot remove field \"sessions\" from \"usage.daily_rollup\" — it already has ingested data."}}},"segmentInUse":{"summary":"Unpromoting a field a segment depends on","value":{"error":{"code":"VALIDATION_ERROR","message":"Cannot unpromote field \"sessions\" on \"usage.daily_rollup\" — 2 saved segment(s) filter or display it."}}}}}}},"404":{"description":"Event type not found"}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/schemas/sources/{sourceType}/editability":{"get":{"tags":["ackDB"],"summary":"Discover what may currently be edited on a source and its events","description":"Returns, per event type, which keys are locked right now and a human-readable reason for each, so a client can render disabled controls instead of discovering the rules by provoking 400s. Permanent locks (rename, field-type change, optional→required) are always present; data-gated locks (eventScope, field/mapping removal, new required field, externalIdScope) appear once the event type holds stored data. Also reports the source-level immutables and the fact that event-type DELETE has no endpoint.","operationId":"getSourceEditability","parameters":[{"name":"sourceType","in":"path","required":true,"description":"Source type (e.g. `stripe`)","schema":{"type":"string"},"example":"stripe"}],"responses":{"200":{"description":"Editability contract for the source","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"}},"required":["data"]},"example":{"data":{"sourceType":"stripe","hasIngestedData":true,"userModified":false,"immutable":["sourceType","events[].eventType"],"unsupported":["delete an event type — no endpoint exists; delete the whole source instead"],"events":[{"eventType":"invoice.paid","hasStoredData":true,"userModified":false,"locked":[{"key":"eventType","reason":"Event types cannot be renamed."},{"key":"fields[].type","reason":"An existing field's type can never change."},{"key":"eventScope","reason":"This event type has ingested data."}]}]}}}}},"404":{"description":"Source not found"}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/schemas/sources/{sourceType}/enable":{"post":{"tags":["ackDB"],"summary":"Enable a source","description":"Copies default traits from the source definition into the trait_definitions table. Idempotent - skips traits that already exist. Call this after adding a new source definition to populate its default traits.\n\nNOTE — this does NOT toggle the `disabled` flag. Since shipped packs ship `disabled: true` on a fresh deployment, seeding traits here does not by itself let the source accept ingest. To activate a source for ingest, set its `disabled` flag off: `PUT /schemas/sources/{sourceType} { \"disabled\": false }`.","operationId":"enableSource","parameters":[{"name":"sourceType","in":"path","required":true,"description":"Source type to enable (e.g. `slack`, `stripe`)","schema":{"type":"string"},"example":"slack"}],"responses":{"200":{"description":"Source enabled, default traits created","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"sourceType":{"type":"string"},"traitsCreated":{"type":"integer","description":"Number of new trait definitions created"},"traits":{"type":"array","items":{"$ref":"#/components/schemas/AckdbTraitDefinition"}}},"required":["sourceType","traitsCreated","traits"]}},"required":["data"]}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/schemas/identity-link-types":{"get":{"tags":["ackDB"],"summary":"List universal identity link types","description":"Universal identity link types (#310) resolve or match an entity from ANY source, not just the one that declared it — like `domain`, but tenant-defined. The list always starts with the built-in `domain` entry (`builtIn: true`, synthesized, never stored in this table), followed by every registered type (`builtIn: false`). Registered types are match-only by default; an entity-scoped type flagged `canMint: true` (#451) is a secondary anchor that can also CREATE an entity — at ingest and via `POST /entities` — for companies that have no domain. `POST /entities/{ref}/links` still rejects writes to `domain` (422).","operationId":"listIdentityLinkTypes","responses":{"200":{"description":"List of universal identity link types, `domain` first","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbIdentityLinkTypeDefinition"}}},"required":["data"]},"example":{"data":[{"typeKey":"domain","description":"Primary universal identifier — the company's website domain. Valid from any source and the primary anchor that CREATES an entity at ingest.","canMint":true,"builtIn":true},{"typeKey":"duns_number","description":"D-U-N-S business identifier","canMint":false,"builtIn":false,"createdAt":"2026-07-09T00:00:00.000Z","updatedAt":"2026-07-09T00:00:00.000Z"}]}}}}},"security":[{"apiKey":[]}]},"post":{"tags":["ackDB"],"summary":"Register a universal identity link type","description":"Registers a new universal identity link type. `key` is trimmed then validated against `^[a-z][a-z0-9_]*$`. Rejects `domain` (built-in, always universal) and any key already declared per-source by a source definition — one namespace: a per-source type name cannot also be registered as universal, and vice versa (see `PUT /schemas/sources/{sourceType}`). `canMint: true` (#451) registers the type as a secondary anchor that may CREATE an entity — entity scope only; the combination with `scope: \"person\"` is rejected (400).","operationId":"createIdentityLinkType","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbIdentityLinkTypeCreateRequest"},"example":{"key":"duns_number","description":"D-U-N-S business identifier"}}}},"responses":{"201":{"description":"Identity link type registered","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbIdentityLinkTypeDefinition"}},"required":["data"]},"example":{"data":{"typeKey":"duns_number","description":"D-U-N-S business identifier","builtIn":false,"createdAt":"2026-07-09T00:00:00.000Z","updatedAt":"2026-07-09T00:00:00.000Z"}}}}},"400":{"description":"Missing/blank `key`, or `key` does not match `^[a-z][a-z0-9_]*$`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"},"example":{"error":{"code":"VALIDATION_ERROR","message":"key must match /^[a-z][a-z0-9_]*$/"}}}}},"409":{"description":"`domain` is reserved (`LINK_TYPE_RESERVED`), the key is already registered (`LINK_TYPE_EXISTS`), or the key is already declared per-source by a source definition (`LINK_TYPE_CONFLICT`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"},"examples":{"reserved":{"value":{"error":{"code":"LINK_TYPE_RESERVED","message":"\"domain\" is built-in and always universal."}}},"exists":{"value":{"error":{"code":"LINK_TYPE_EXISTS","message":"Identity link type \"duns_number\" is already registered."}}},"conflict":{"value":{"error":{"code":"LINK_TYPE_CONFLICT","message":"\"slack_channel_id\" is already declared by source(s): slack. Per-source types cannot be re-registered as universal."}}}}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/schemas/identity-link-types/{typeKey}":{"get":{"tags":["ackDB"],"summary":"Get a universal identity link type","description":"`typeKey: \"domain\"` returns the built-in entry (`builtIn: true`).","operationId":"getIdentityLinkType","parameters":[{"name":"typeKey","in":"path","required":true,"description":"The link type key (e.g. `duns_number`, or `domain` for the built-in).","schema":{"type":"string"},"example":"duns_number"}],"responses":{"200":{"description":"The identity link type","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbIdentityLinkTypeDefinition"}},"required":["data"]}}}},"404":{"description":"No universal identity link type registered under this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"},"example":{"error":{"code":"LINK_TYPE_NOT_FOUND","message":"Identity link type not found: duns_number"}}}}}},"security":[{"apiKey":[]}]},"patch":{"tags":["ackDB"],"summary":"Update a universal identity link type","description":"Updates the description, scope, and/or `canMint` flag (#451). `domain` is built-in; it is fixed and this always 409s for it. A person-scoped type can never end up mintable — `canMint: true` on (or a scope flip to) a person-scoped type is rejected (400, `LINK_TYPE_SCOPE_MINT_CONFLICT`). Turning `canMint` off is always safe: entities already minted through the type keep their links; only future minting stops.","operationId":"updateIdentityLinkType","parameters":[{"name":"typeKey","in":"path","required":true,"description":"The link type key to update.","schema":{"type":"string"},"example":"duns_number"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbIdentityLinkTypeUpdateRequest"},"example":{"description":"D-U-N-S Number — Dun & Bradstreet business identifier"}}}},"responses":{"200":{"description":"Updated identity link type","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbIdentityLinkTypeDefinition"}},"required":["data"]}}}},"404":{"description":"No universal identity link type registered under this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"409":{"description":"`typeKey` is `domain` — its description is fixed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"},"example":{"error":{"code":"LINK_TYPE_RESERVED","message":"\"domain\" is built-in; its description is fixed."}}}}}},"security":[{"apiKey":[]}]},"delete":{"tags":["ackDB"],"summary":"Delete a universal identity link type","description":"Blocked for `domain` (built-in) and for any type that still has link values on companies or contacts — unlink every value first (`DELETE /entities/{ref}/links/{type}/{value}` and/or `DELETE /contacts/{contactId}/links/{type}/{value}`), then delete the type.","operationId":"deleteIdentityLinkType","parameters":[{"name":"typeKey","in":"path","required":true,"description":"The link type key to delete.","schema":{"type":"string"},"example":"duns_number"}],"responses":{"204":{"description":"Identity link type deleted"},"404":{"description":"No universal identity link type registered under this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"409":{"description":"`typeKey` is `domain` (`LINK_TYPE_RESERVED`), or the type still has link values (`LINK_TYPE_IN_USE` — the error body carries `counts: { entityLinks, contactLinks }`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"},"examples":{"reserved":{"value":{"error":{"code":"LINK_TYPE_RESERVED","message":"\"domain\" is built-in and cannot be deleted."}}},"inUse":{"value":{"error":{"code":"LINK_TYPE_IN_USE","message":"\"duns_number\" still has link values (2 company, 1 contact). Delete those first.","counts":{"entityLinks":2,"contactLinks":1}}}}}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/schemas/traits":{"get":{"tags":["ackDB"],"summary":"List trait definitions","description":"Returns trait definitions. This is how AI agents discover what fields are available. Pass `?source=<sourceType>` to scope the result to a single source's traits (e.g. for a per-source ingest example); omit it for every trait. Returns an empty array if no matching trait definitions exist (e.g. no sources enabled yet, or a source that registers no traits).","operationId":"listTraits","parameters":[{"name":"source","in":"query","required":false,"description":"Scope to a single source type (e.g. `slack`). Omit to list every trait.","schema":{"type":"string"},"example":"slack"}],"responses":{"200":{"description":"List of trait definitions (may be empty)","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbTraitDefinition"}}},"required":["data"]},"example":{"data":[{"traitKey":"slack_message_count","valueType":"number","source":"slack","description":"Total messages in linked Slack channels","allowedValues":null,"createdAt":"2026-03-08T00:00:00.000Z"}]}}}}},"security":[{"apiKey":[]}]},"post":{"tags":["ackDB"],"summary":"Create a trait definition","description":"Creates a new trait definition. Use this to register custom traits beyond the defaults provided by source definitions. If the trait already exists, it will be updated.","operationId":"createTrait","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"traitKey":{"type":"string","description":"Unique trait key identifier.","example":"custom_health_score"},"valueType":{"type":"string","enum":["string","number","boolean","date","string_array"],"description":"Data type for this trait's values.","example":"number"},"source":{"type":"string","description":"Source that produces this trait.","example":"custom"},"description":{"type":["string","null"],"description":"Human/AI-readable description of what this trait means.","example":"Overall account health score (0-100)"},"allowedValues":{"type":["array","null"],"description":"If set, restricts trait values to this list.","items":{},"example":null}},"required":["traitKey","valueType","source"]}}}},"responses":{"201":{"description":"Trait definition created","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbTraitDefinition"}},"required":["data"]}}}},"400":{"description":"Validation error - missing required fields","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/schemas/traits/{traitKey}":{"get":{"tags":["ackDB"],"summary":"Get a single trait definition","description":"Returns a specific trait definition by trait key. Use this for targeted lookups when you know the exact trait you need.","operationId":"getTrait","parameters":[{"name":"traitKey","in":"path","required":true,"description":"Trait key identifier (e.g. `slack_message_count`, `stripe_plan`)","schema":{"type":"string"},"example":"slack_message_count"}],"responses":{"200":{"description":"Trait definition found","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbTraitDefinition"}},"required":["data"]}}}},"404":{"description":"Trait not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"},"example":{"error":{"code":"TRAIT_NOT_FOUND","message":"Trait not found: slack_message_count"}}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/ingest/logs":{"get":{"tags":["ackDB"],"summary":"Query ingest logs","description":"Returns paginated ingest logs with optional filters. Each log captures one ingest HTTP request's full lifecycle: validation timing, entity resolution path, write counts, and error details.","operationId":"queryIngestLogs","parameters":[{"name":"entityId","in":"query","schema":{"type":"string"},"description":"Filter by entity ID"},{"name":"source","in":"query","schema":{"type":"string"},"description":"Filter by source type"},{"name":"status","in":"query","schema":{"type":"string","enum":["success","deduplicated","unresolved","error"]},"description":"Filter by status"},{"name":"since","in":"query","schema":{"type":"string","format":"date-time"},"description":"Start of date range"},{"name":"until","in":"query","schema":{"type":"string","format":"date-time"},"description":"End of date range"},{"name":"limit","in":"query","schema":{"type":"integer","default":50}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}}],"responses":{"200":{"description":"Paginated ingest logs","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbIngestLog"}},"pagination":{"$ref":"#/components/schemas/AckdbPagination"}}}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/ingest/logs/{id}":{"get":{"tags":["ackDB"],"summary":"Get a single ingest log","operationId":"getIngestLog","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Ingest log ID"}],"responses":{"200":{"description":"Ingest log found","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbIngestLog"}}}}}},"404":{"description":"Ingest log not found"}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/unresolved":{"get":{"tags":["ackDB"],"summary":"List unresolved content","description":"Returns unresolved content records - data that arrived via `POST /ingest` without a `domain` identity link, so it couldn't be automatically resolved to an entity. Filterable by source type and date range.","operationId":"listUnresolved","parameters":[{"name":"source","in":"query","required":false,"description":"Filter by source type (e.g. `slack`, `stripe`)","schema":{"type":"string"}},{"name":"since","in":"query","required":false,"description":"Filter records created after this ISO date","schema":{"type":"string","format":"date-time"}},{"name":"until","in":"query","required":false,"description":"Filter records created before this ISO date","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"List of unresolved content records","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbUnresolvedContent"}}},"required":["data"]}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/unresolved/stats":{"get":{"tags":["ackDB"],"summary":"Get unresolved content stats","description":"Returns summary counts of unresolved content by source type. Useful for dashboards showing 'Slack has 20 unresolved channels, Stripe has 3'.","operationId":"getUnresolvedStats","responses":{"200":{"description":"Unresolved content statistics","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"total":{"type":"integer","description":"Total unresolved records","example":23},"bySource":{"type":"object","description":"Count per source type","additionalProperties":{"type":"integer"},"example":{"slack":20,"stripe":3}}},"required":["total","bySource"]}},"required":["data"]}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/{entityId}/files":{"get":{"tags":["ackDB"],"summary":"List files for an entity","description":"List files (images, PDFs, documents) attached to an entity. Supports filtering by source, MIME type, and processing status.","parameters":[{"name":"entityId","in":"path","required":true,"schema":{"type":"string"},"description":"Entity ID"},{"name":"source","in":"query","schema":{"type":"string"},"description":"Filter by source"},{"name":"fileType","in":"query","schema":{"type":"string"},"description":"Filter by MIME type prefix (e.g., 'image/', 'application/pdf')"},{"name":"processed","in":"query","schema":{"type":"boolean"},"description":"Filter by processing status"},{"name":"limit","in":"query","schema":{"type":"integer","default":50},"description":"Max results"},{"name":"offset","in":"query","schema":{"type":"integer","default":0},"description":"Pagination offset"}],"responses":{"200":{"description":"List of files with metadata.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbFileMetadata"}},"pagination":{"$ref":"#/components/schemas/AckdbPagination"}}}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/files/{fileId}":{"get":{"tags":["ackDB"],"summary":"Get file metadata","description":"Get detailed metadata for a specific file, including AI-generated description and extracted text.","parameters":[{"name":"fileId","in":"path","required":true,"schema":{"type":"string"},"description":"File ID"}],"responses":{"200":{"description":"File metadata with description and extracted text.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbFileMetadataDetail"}}}}}},"404":{"description":"File not found."}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/files/{fileId}/content":{"get":{"tags":["ackDB"],"summary":"Download file content","description":"Stream the binary file content. Returns the file with its original MIME type.","parameters":[{"name":"fileId","in":"path","required":true,"schema":{"type":"string"},"description":"File ID"}],"responses":{"200":{"description":"File binary content.","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"404":{"description":"File not found."}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/data/events":{"delete":{"tags":["ackDB"],"summary":"Delete specific events by id (with cascade)","description":"Surgically delete events by id. Events are append-only - this is the supported way to remove bad data (test / duplicate / wrong-entity events) and to correct a wrong-field event: **delete it, then re-ingest the corrected one.**\n\n**Why delete enables correction:** the idempotency key lives on the event's raw payload. Re-pushing a corrected event with the same key is normally deduplicated (silently dropped). Deleting the event also deletes its raw payload, which frees the idempotency key - so the corrected re-push is accepted as a new event.\n\n**What gets deleted per event (atomic, one transaction):** the event row, its raw payload (frees the idempotency key), its attached files (rows + stored blobs), the affected entity+source pairs' embedding chunks (except chunks a surviving file still references), and every conversation built from a deleted payload — plus an audit row in `event_deletions`.\n\n**Derived content is invalidated, never left stale:** deleted text must not stay semantically searchable or citable. Surviving payloads of the affected pairs are queued for re-embedding (`pendingRebuild` reports each pair); until the embed/conversations pipelines next run, search for those pairs is missing — deliberately.\n\n**Refusals (409, whole request, fail-closed):** `SYSTEM_EVENT_NOT_DELETABLE` when any id is an ackDB bookkeeping event (source `system`/`member` — the audit log of state that still stands; undo the state via its inverse API instead). `PENDING_INGEST_CONFLICT` when a queued ingest shares an idempotency key with a to-be-deleted payload and would resurrect it (retry after the queue drains).\n\n**Deliberately retained (reported in `retained`):** the affected entities' identity links — they are why a corrected re-ingest lands on the SAME entity — and any entity the delete leaves without source events/payloads (entity delete does not exist; the row stays, resolvable via its links).\n\n**IDs-only by design** - no filter/query selectors. Look up ids first via the read API. Unknown ids are reported in `notFound`, never fatal. Use `dryRun: true` to preview the identical report without mutating. **This is a destructive, irreversible operation** (re-ingest from the source to recover).","operationId":"deleteEvents","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbDeleteEventsRequest"},"examples":{"dryRun":{"summary":"Preview a delete (no mutation)","value":{"ids":["01J9ABCDEF0123456789ABCDEF","01J9ZYXWVU9876543210ZYXWVU"],"dryRun":true}},"commit":{"summary":"Delete two events","value":{"ids":["01J9ABCDEF0123456789ABCDEF","01J9ZYXWVU9876543210ZYXWVU"],"reason":"duplicate Slack import"}}}}}},"responses":{"200":{"description":"Delete (or dry-run preview) completed. Returns per-table counts, what was deliberately retained, and the per-pair pending-rebuild report.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbDeleteEventsResponse"},"examples":{"committed":{"summary":"Two events deleted","value":{"data":{"dryRun":false,"deleted":{"events":2,"rawPayloads":2,"files":1,"contentChunks":3,"conversations":1,"blobs":1},"notFound":[],"retained":[{"kind":"identity_links","entityId":"01H8XGJWBWBAQ4ZZTW1MD8K9P2","count":2,"reason":"kept so a corrected re-ingest resolves to the same entity instead of minting a duplicate"}],"pendingRebuild":[{"entityId":"01H8XGJWBWBAQ4ZZTW1MD8K9P2","source":"slack","contentChunksDeleted":3,"conversationsDeleted":1,"payloadsQueuedForReembed":14}],"rebuildNote":"Embedding chunks and conversation summaries for these entity+source pairs were removed with the events. Semantic search and conversation answers for them are MISSING until the pipelines next run (npm run embed / npm run conversations) — missing is deliberate; deleted text must never stay searchable."}}},"dryRunPreview":{"summary":"Dry-run preview","value":{"data":{"dryRun":true,"resolved":[{"id":"01J9ABCDEF0123456789ABCDEF","entityId":"01H8XGJWBWBAQ4ZZTW1MD8K9P2","source":"slack","eventType":"message","summary":"Test message from Acme Corp","timestamp":"2026-05-01T10:00:00.000Z","linkedRawPayloadId":"01H8XGJZZ4Q9P2RST7VWXY3456","fileCount":0}],"notFound":["01J9ZYXWVU9876543210ZYXWVU"],"wouldDelete":{"events":1,"rawPayloads":1,"files":0,"contentChunks":2,"conversations":1,"blobs":0},"retained":[{"kind":"identity_links","entityId":"01H8XGJWBWBAQ4ZZTW1MD8K9P2","count":2,"reason":"kept so a corrected re-ingest resolves to the same entity instead of minting a duplicate"}],"pendingRebuild":[{"entityId":"01H8XGJWBWBAQ4ZZTW1MD8K9P2","source":"slack","contentChunksDeleted":2,"conversationsDeleted":1,"payloadsQueuedForReembed":14}]}}}}}}},"400":{"description":"Invalid request body - empty `ids`, more than 1000 ids, or malformed JSON.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"409":{"description":"Refused fail-closed (no partial deletes): `SYSTEM_EVENT_NOT_DELETABLE` — a requested id is an ackDB bookkeeping event (source `system`/`member`); the offending ids are in `error.details.events`, undo the state via its inverse API instead. `PENDING_INGEST_CONFLICT` — a queued ingest shares an idempotency key with a to-be-deleted payload; retry after the queue drains.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/lookup":{"get":{"tags":["ackDB"],"summary":"Look up entity by external ID","description":"Check if a source-specific ID (Slack channel, Stripe customer) or universal ID (domain, email) is already linked to an entity in ackDB. Always returns 200 - 'not found' is a valid lookup result, not an error.\n\n**The resolve-once pattern:**\n1. First event from a source (e.g., Slack channel `C04ABC123`) → workflow resolves the company domain, ingests with `domain` identity link\n2. Subsequent events from the same channel → workflow calls `GET /entities/lookup?externalIdType=slack_channel_id&externalIdValue=C04ABC123&source=slack` → gets the `entityId` back → ingests with `entityId` directly, skipping domain resolution\n\n**Three possible outcomes:**\n- **Found:** Identity link exists → returns entity ID, name, domain, and link timestamp\n- **Unresolved:** ID exists in unresolved content pool (ingested without domain) → returns unresolved content IDs\n- **Never seen:** ID not in ackDB at all → returns `match: null, unresolvedMatches: []`","operationId":"lookupIdentity","parameters":[{"name":"externalIdType","in":"query","required":true,"description":"Identity link type to look up. Universal types (e.g., `domain`) work without a `source` param. Source-specific types (e.g., `slack_channel_id`, `stripe_customer_id`) require the `source` param.","schema":{"type":"string"},"examples":{"domain":{"value":"domain","summary":"Universal - company domain"},"slack":{"value":"slack_channel_id","summary":"Source-specific - Slack channel"},"stripe":{"value":"stripe_customer_id","summary":"Source-specific - Stripe customer"}}},{"name":"externalIdValue","in":"query","required":true,"description":"The actual external ID value to look up.","schema":{"type":"string"},"examples":{"domain":{"value":"acme.com","summary":"Domain lookup"},"slack":{"value":"C04ABC123","summary":"Slack channel ID"},"stripe":{"value":"cus_OqN8hNPayLBjXF","summary":"Stripe customer ID"}}},{"name":"source","in":"query","required":false,"description":"Source type (e.g., `slack`, `stripe`). Required for source-specific ID types. Omit for universal types like `domain` - the lookup will search across all sources.","schema":{"type":"string"},"examples":{"slack":{"value":"slack","summary":"Slack source"},"stripe":{"value":"stripe","summary":"Stripe source"}}}],"responses":{"200":{"description":"Lookup result - always 200, check `match` field for result","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"match":{"type":["object","null"],"description":"The matched entity, or null if no entity found.","properties":{"entityId":{"type":"string","example":"01HXYZ123456789ABCDEFGH"},"entityName":{"type":["string","null"],"example":"acme.com"},"domain":{"type":["string","null"],"example":"acme.com"},"linkedAt":{"type":"string","format":"date-time"}},"required":["entityId","entityName","domain","linkedAt"]},"unresolvedMatches":{"type":"array","description":"Unresolved content records matching this ID (when no entity match).","items":{"type":"object","properties":{"unresolvedContentId":{"type":"string"},"source":{"type":"string"},"timestamp":{"type":"string","format":"date-time"}},"required":["unresolvedContentId","source","timestamp"]}}},"required":["match","unresolvedMatches"]}},"required":["data"]},"examples":{"found":{"summary":"Entity found","value":{"data":{"match":{"entityId":"01HXYZ123456789ABCDEFGH","entityName":"acme.com","domain":"acme.com","linkedAt":"2026-03-15T10:30:00.000Z"},"unresolvedMatches":[]}}},"unresolved":{"summary":"Found in unresolved pool","value":{"data":{"match":null,"unresolvedMatches":[{"unresolvedContentId":"01HXYZ123456789ABCDEFGI","source":"slack","timestamp":"2026-03-15T10:30:00.000Z"}]}}},"never_seen":{"summary":"Never seen","value":{"data":{"match":null,"unresolvedMatches":[]}}}}}}},"422":{"description":"Missing required parameters","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","example":"MISSING_PARAMS"},"message":{"type":"string","example":"Both externalIdType and externalIdValue are required"}},"required":["error","message"]}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities":{"get":{"tags":["ackDB"],"summary":"List and filter entities","description":"Returns a paginated list of entities. Supports text search across entity names and multi-trait filtering via `traitFilter` query params.\n\n**Trait filters:** Each `traitFilter` param uses `traitKey:operator:value` format. Multiple filters are combined with AND logic.\n\n**Operators:** `eq`, `neq` (string/text comparison), `gt`, `gte`, `lt`, `lte` (numeric), `is_null`, `is_not_null` (existence check - no value needed).\n\n**Examples:**\n- `?traitFilter=stripe_plan:eq:enterprise` - entities on enterprise plan\n- `?traitFilter=stripe_total_paid_cents:gte:10000` - entities that paid >= $100\n- `?traitFilter=stripe_plan:eq:enterprise&traitFilter=stripe_status:eq:active` - enterprise + active\n- `?traitFilter=stripe_plan:is_null` - entities without a Stripe plan\n\n**Hidden companies (#493, #505):** a company whose only ingested evidence comes from a source flagged `contextOnly` (`job` → a past employer, `education` → a school) is context on somebody's record, not an account. A company minted solely as a relationship endpoint is also hidden until real evidence arrives. Both are EXCLUDED from this browse list; pass `includeReference=true` to include them. They remain reachable by id, by domain, and via `GET /entities/search`.\n\n**Internal companies (#567):** a company holding a `domain` link listed in `internal_domains` is the deployment ITSELF, never an account. Same treatment: excluded here, revealed by `includeReference=true`, always reachable by id/domain/search. Unlike a reference company it is never promoted by activity — data landing on your own domain is still your own domain.","operationId":"listEntities","parameters":[{"name":"search","in":"query","required":false,"description":"Search filter applied to entity name (case-insensitive partial match).","schema":{"type":"string"}},{"name":"traitFilter","in":"query","required":false,"description":"Trait filter in `traitKey:operator:value` format. Repeat for multiple filters (AND logic). Operators: eq, neq, gt, gte, lt, lte, is_null, is_not_null.","schema":{"type":"array","items":{"type":"string"}},"style":"form","explode":true},{"name":"limit","in":"query","required":false,"description":"Maximum number of entities to return.","schema":{"type":"integer","default":50}},{"name":"offset","in":"query","required":false,"description":"Number of entities to skip for pagination.","schema":{"type":"integer","default":0}},{"name":"includeReference","in":"query","required":false,"description":"Include hidden companies: reference companies (their only ingested evidence comes from a `contextOnly` source — job history or education, #493), internal companies (a `domain` link in `internal_domains`, #567), and graph-only relationship endpoints (#505). All are excluded by default.","schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"Paginated list of entities","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbEntity"}},"pagination":{"$ref":"#/components/schemas/AckdbPaginationMeta"}},"required":["data","pagination"]}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/summary":{"get":{"tags":["ackDB"],"summary":"All entities with traits (bulk)","description":"Returns all active entities with their traits in a single response. One SQL query, no N+1. Use for dashboards, reports, bulk comparisons.","operationId":"getEntitiesSummary","responses":{"200":{"description":"All entities with their traits","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"entityId":{"type":"string","example":"01HXYZ123456789ABCDEFGH"},"name":{"type":"string","example":"acme.com"},"createdAt":{"type":"string","format":"date-time"},"traits":{"type":"object","description":"Key-value map of trait_key → trait value (stringified).","additionalProperties":{},"example":{"stripe_plan":"enterprise","slack_message_count":"42"}}},"required":["entityId","name","createdAt","traits"]}},"total":{"type":"integer","description":"Number of entities returned.","example":12}},"required":["data","total"]}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/events":{"get":{"tags":["ackDB"],"summary":"Cross-entity event search","description":"Search events across all entities. Filterable by source, event type, and date range. Returns events with entity names. Use for cross-entity queries like 'all Stripe refunds in February' without per-entity N+1 calls.","operationId":"searchEvents","parameters":[{"name":"source","in":"query","required":false,"description":"Filter by source type (e.g. `slack`, `stripe`).","schema":{"type":"string"}},{"name":"eventType","in":"query","required":false,"description":"Filter by event type (e.g. `stripe_charge.refunded`, `slack_message`).","schema":{"type":"string"}},{"name":"since","in":"query","required":false,"description":"Filter events after this ISO date.","schema":{"type":"string","format":"date-time"}},{"name":"until","in":"query","required":false,"description":"Filter events before this ISO date.","schema":{"type":"string","format":"date-time"}},{"name":"limit","in":"query","required":false,"description":"Maximum number of events to return (max 200).","schema":{"type":"integer","default":50}},{"name":"offset","in":"query","required":false,"description":"Number of events to skip for pagination. Cannot be combined with `cursor` (400).","schema":{"type":"integer","default":0}},{"name":"cursor","in":"query","required":false,"description":"Crawl mode: pass an EMPTY value to start, then each response's `pagination.nextCursor`. Pages are keyset-read by event id (oldest id first) so any depth costs the same — use this to walk a whole source. `pagination` becomes `{ limit, nextCursor, total? }` (`total` on the first page only, `nextCursor` null when done). Omit for the default timestamp-desc / offset paging.","schema":{"type":"string"}},{"name":"graphRoot","in":"query","schema":{"type":"string"},"description":"Root entity id or domain."},{"name":"graphScope","in":"query","schema":{"type":"string","enum":["parents","children","connected"]},"description":"Required when graphRoot is supplied."},{"name":"graphMaxDepth","in":"query","schema":{"type":"integer","minimum":0,"maximum":499}},{"name":"graphAsOf","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"graphRelationshipType","in":"query","schema":{"type":"array","items":{"type":"string"}},"style":"form","explode":true},{"name":"personId","in":"query","required":false,"schema":{"type":"string"},"description":"Filter events stamped with this human; merged IDs resolve to their survivor."},{"name":"ownership","in":"query","required":false,"schema":{"type":"string","enum":["person","company"]},"description":"person selects sole-person events; company selects company-associated events; omit for both. Filters before pagination/counts. Personal ownership conflicts with entityId/company graph."}],"responses":{"200":{"description":"Paginated list of events across entities. Default mode: `pagination: { total, limit, offset }`. Crawl mode (`cursor` given): `pagination: { limit, nextCursor, total? }`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/AckdbEntityEvent"}],"type":"object","properties":{"entityName":{"type":["string","null"]},"personName":{"type":["string","null"]}},"required":["entityName"]}},"pagination":{"oneOf":[{"$ref":"#/components/schemas/AckdbPaginationMeta"},{"type":"object","description":"Crawl-mode pagination (`cursor` given).","properties":{"limit":{"type":"integer","example":200},"nextCursor":{"type":"string","nullable":true,"description":"Pass back as `cursor` for the next page; null when the crawl is complete.","example":"01M04FNQ7SYNPKB9D7EX2DPVDE"},"total":{"type":"integer","description":"Exact filtered total — first page (empty `cursor`) only.","example":412000}},"required":["limit","nextCursor"]}]}},"required":["data","pagination"]},"example":{"data":[{"id":"01HXYZ123456789ABCDEFGH","entityId":"01HXYZ123456789ABCDEFGI","entityName":"acme.com","source":"stripe","eventType":"stripe_charge.refunded","summary":"Stripe refund: $50.00 USD","metadata":{"refund_amount_cents":5000,"currency":"usd","refund_full":true},"timestamp":"2026-02-15T14:30:00.000Z","createdAt":"2026-02-15T14:30:01.000Z"}],"pagination":{"total":1,"limit":50,"offset":0}}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/events/aggregate":{"get":{"tags":["ackDB"],"summary":"Cross-entity event aggregation","description":"Aggregate events across all entities. Returns counts, distinct entity counts, and optional SUM/AVG/MIN/MAX. Supports GROUP BY event_type, source, month, or entity. Use for analytics like 'total revenue by month' or 'refund count per entity'.\n\n**sumField:** Name of a promoted metadata field for numeric aggregation (e.g. `amount_cents`). Enables SUM, AVG, MIN, MAX in the response. Only works on numeric metadata fields.\n\n**minCount:** Minimum event count threshold (HAVING COUNT >= N). Only groups with at least this many events are returned.\n\n**minSum:** Minimum sum threshold (HAVING SUM >= N). Requires sumField. Only groups whose sum meets this threshold are returned. Use with `groupBy=entity` to find entities above a revenue threshold (e.g., `minSum=100000` for > $1000 in cents).","operationId":"aggregateEvents","parameters":[{"name":"source","in":"query","required":false,"description":"Filter by source type.","schema":{"type":"string"}},{"name":"eventType","in":"query","required":false,"description":"Filter by event type.","schema":{"type":"string"}},{"name":"since","in":"query","required":false,"description":"Filter events after this ISO date.","schema":{"type":"string","format":"date-time"}},{"name":"until","in":"query","required":false,"description":"Filter events before this ISO date.","schema":{"type":"string","format":"date-time"}},{"name":"groupBy","in":"query","required":false,"description":"Group results by a dimension. Person/entity groups exclude events without that owner. Ungrouped totals count all matches.","schema":{"type":"string","enum":["event_type","source","month","entity","person"]}},{"name":"sumField","in":"query","required":false,"description":"Promoted metadata field for numeric aggregation (e.g. `amount_cents`). Enables SUM, AVG, MIN, MAX.","schema":{"type":"string"}},{"name":"minCount","in":"query","required":false,"description":"Minimum event count threshold (HAVING COUNT >= N). Only groups meeting this threshold are returned.","schema":{"type":"integer","minimum":1}},{"name":"minSum","in":"query","required":false,"description":"Minimum sum threshold (HAVING SUM >= N). Requires sumField. Only groups whose sum meets this threshold are returned.","schema":{"type":"number"}},{"name":"graphRoot","in":"query","schema":{"type":"string"},"description":"Root entity id or domain."},{"name":"graphScope","in":"query","schema":{"type":"string","enum":["parents","children","connected"]},"description":"Required when graphRoot is supplied."},{"name":"graphMaxDepth","in":"query","schema":{"type":"integer","minimum":0,"maximum":499}},{"name":"graphAsOf","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"graphRelationshipType","in":"query","schema":{"type":"array","items":{"type":"string"}},"style":"form","explode":true},{"name":"personId","in":"query","required":false,"schema":{"type":"string"},"description":"Filter events stamped with this human; merged IDs resolve to their survivor."},{"name":"ownership","in":"query","required":false,"schema":{"type":"string","enum":["person","company"]},"description":"person selects sole-person events; company selects company-associated events; omit for both. Filters before pagination/counts. Personal ownership conflicts with entityId/company graph."}],"responses":{"200":{"description":"Aggregation results","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"count":{"type":"integer","description":"Total event count matching filters.","example":45},"distinctEntities":{"type":"integer","description":"Number of unique entities with matching events.","example":12},"sum":{"type":"number","description":"Sum of the specified metadata field (only present when sumField is provided).","example":1250000},"avg":{"type":"number","description":"Average of the specified metadata field (only present when sumField is provided).","example":27777.78},"min":{"type":"number","description":"Minimum value of the specified metadata field (only present when sumField is provided).","example":5000},"max":{"type":"number","description":"Maximum value of the specified metadata field (only present when sumField is provided).","example":99000},"groups":{"type":"array","description":"Grouped results (only present when groupBy is provided).","items":{"type":"object","properties":{"key":{"type":"string","description":"Group key value.","example":"stripe_charge.succeeded"},"count":{"type":"integer","example":30},"distinctEntities":{"type":"integer","example":10},"sum":{"type":"number","description":"Sum within this group (if sumField specified)."},"avg":{"type":"number","description":"Average within this group (if sumField specified)."},"min":{"type":"number","description":"Minimum within this group (if sumField specified)."},"max":{"type":"number","description":"Maximum within this group (if sumField specified)."},"distinctPersons":{"type":"integer"},"personId":{"type":["string","null"]},"personName":{"type":["string","null"]},"entityId":{"type":["string","null"]},"entityName":{"type":["string","null"]}},"required":["key","count","distinctEntities"]}},"distinctPersons":{"type":"integer"}},"required":["count","distinctEntities"]}},"required":["data"]},"examples":{"simple":{"summary":"Simple count","value":{"data":{"count":45,"distinctEntities":12}}},"grouped":{"summary":"Grouped by event type with SUM","value":{"data":{"count":45,"distinctEntities":12,"sum":1250000,"groups":[{"key":"stripe_charge.succeeded","count":30,"distinctEntities":10,"sum":1200000},{"key":"stripe_charge.refunded","count":15,"distinctEntities":8,"sum":50000}]}}}}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/events/sequence":{"get":{"tags":["ackDB"],"summary":"Find cross-source temporal event sequences","description":"Find ordered event types within windowDays for the same non-null company or person selected by grain. Person grain joins personal and company-associated history for one human; company graph is incompatible. Ownership/personId filters apply to both sides. One earliest sequence per owner.","operationId":"findEventSequences","parameters":[{"name":"firstEventType","in":"query","required":true,"description":"The event type that happens first (e.g., 'stripe_charge.refunded').","schema":{"type":"string"}},{"name":"secondEventType","in":"query","required":true,"description":"The event type that happens after (e.g., 'slack_message').","schema":{"type":"string"}},{"name":"windowDays","in":"query","required":false,"description":"Maximum days between events (default 7).","schema":{"type":"integer","default":7,"minimum":1,"maximum":365}},{"name":"firstSource","in":"query","required":false,"description":"Filter first event by source.","schema":{"type":"string"}},{"name":"secondSource","in":"query","required":false,"description":"Filter second event by source.","schema":{"type":"string"}},{"name":"since","in":"query","required":false,"description":"Only consider first events on or after this date.","schema":{"type":"string","format":"date-time"}},{"name":"until","in":"query","required":false,"description":"Only consider second events on or before this date.","schema":{"type":"string","format":"date-time"}},{"name":"limit","in":"query","required":false,"description":"Maximum results to return.","schema":{"type":"integer","default":50}},{"name":"offset","in":"query","required":false,"description":"Number of results to skip for pagination.","schema":{"type":"integer","default":0}},{"name":"graphRoot","in":"query","schema":{"type":"string"},"description":"Root entity id or domain."},{"name":"graphScope","in":"query","schema":{"type":"string","enum":["parents","children","connected"]},"description":"Required when graphRoot is supplied."},{"name":"graphMaxDepth","in":"query","schema":{"type":"integer","minimum":0,"maximum":499}},{"name":"graphAsOf","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"graphRelationshipType","in":"query","schema":{"type":"array","items":{"type":"string"}},"style":"form","explode":true},{"name":"personId","in":"query","required":false,"schema":{"type":"string"},"description":"Filter events stamped with this human; merged IDs resolve to their survivor."},{"name":"ownership","in":"query","required":false,"schema":{"type":"string","enum":["person","company"]},"description":"person selects sole-person events; company selects company-associated events; omit for both. Filters before pagination/counts. Personal ownership conflicts with entityId/company graph."},{"name":"grain","in":"query","required":false,"schema":{"type":"string","enum":["company","person"],"default":"company"},"description":"Join events within one company or human, retaining each event's actual owner IDs."}],"responses":{"200":{"description":"Event sequences matching the criteria","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"entityId":{"type":["string","null"]},"entityName":{"type":["string","null"]},"daysBetween":{"type":"number","description":"Days between the two events."},"firstEvent":{"$ref":"#/components/schemas/AckdbEntityEvent"},"secondEvent":{"$ref":"#/components/schemas/AckdbEntityEvent"},"grain":{"type":"string","enum":["company","person"]},"personId":{"type":["string","null"]},"personName":{"type":["string","null"]}},"required":["entityId","entityName","daysBetween","firstEvent","secondEvent"]}},"pagination":{"$ref":"#/components/schemas/AckdbPaginationMeta"}},"required":["data","pagination"]}}}},"422":{"description":"Missing required parameters (firstEventType and secondEventType)"}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/{entityId}":{"get":{"tags":["ackDB"],"summary":"Get entity with identity links","description":"Returns a single entity by ID, including all of its identity links. Use this to see which external IDs (Slack channels, Stripe customers, etc.) are linked.","operationId":"getEntity","parameters":[{"name":"entityId","in":"path","required":true,"description":"Entity ID (ULID)","schema":{"type":"string"}}],"responses":{"200":{"description":"Entity found","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","allOf":[{"$ref":"#/components/schemas/AckdbEntity"}],"properties":{"identityLinks":{"type":"array","items":{"$ref":"#/components/schemas/AckdbIdentityLink"}}},"required":["identityLinks"]}},"required":["data"]}}}},"404":{"description":"Entity not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"},"example":{"error":{"code":"ENTITY_NOT_FOUND","message":"Entity not found: 01HXYZ123456789ABCDEFGH"}}}}}},"security":[{"apiKey":[]}]},"delete":{"tags":["ackDB"],"summary":"Delete a company and its entire timeline","operationId":"deleteEntityRecord","description":"Hard-deletes the account and everything on it, archived and restorable, with its identity links frozen as a tombstone so a re-sync cannot re-mint it.\n\n**Goes:** contact edges (+ links, traits, relationship ownership), events, raw payloads, files, traits, identity links, member assignments, embedding chunks + conversations (derived — deleted, never archived), segment memberships (each preceded by an `exit` so Segment Sync consumers do not hold a member that no longer exists), and any human the delete leaves with no affiliation and no evidence anywhere.\n\n**Stays:** file BLOBS (the archive references them; a permanent purge deletes them), `/ask` transcripts (detached — the conversation is the user's, not the record's), and the schema registry, segments and score definitions (config, not data). The unresolved pool is untouched; the tombstone guard fires at promotion time instead.\n\nSystem events (`entity.created`, `member.assigned`, `score.changed`, …) die WITH the record: #419's rule protects the audit log of SURVIVING state, and here the state and its log leave together.","parameters":[{"name":"entityId","in":"path","required":true,"schema":{"type":"string"},"description":"Entity id (ULID) **or** domain — this door resolves either."},{"name":"dryRun","in":"query","required":false,"schema":{"type":"boolean"},"description":"Preview only. Runs the IDENTICAL resolution, cascade and reporting, then rolls back — so the preview is produced by the code that does the work, not by a second implementation that can drift."}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","maxLength":500,"description":"Why — stored on the archive header."},"actor":{"type":"string","description":"Who — console user email; null for service calls."},"dryRun":{"type":"boolean","description":"Same as ?dryRun=true, for clients that cannot send query params."}}},"examples":{"withReason":{"summary":"With an audit trail","value":{"reason":"test account from the June import","actor":"vansh@floqer.com"}}}}}},"responses":{"200":{"description":"Deleted (or previewed). Returns what was deleted, what was detached, what was deliberately retained, and the tombstone that now guards the record's identifiers.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbRecordDeletionResponse"}}}},"404":{"description":"No such record."},"409":{"description":"`MERGED_ENTITY` / `MERGED_PERSON` — the reference names a merge tombstone (delete the survivor; its tombstones go with it). `PENDING_INGEST_CONFLICT` — a queued ingest would resurrect the data or dead-letter on the missing record; retry once the queue drains."}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/{entityId}/traits":{"get":{"tags":["ackDB"],"summary":"Get all traits for an entity","description":"Returns all computed traits for an entity, including provenance (source, workflow, confidence). Each trait has typed value columns - only one of valueString, valueNumber, valueBool, valueDate will be populated based on the trait's type.","operationId":"getEntityTraits","parameters":[{"name":"entityId","in":"path","required":true,"description":"Entity ID (ULID)","schema":{"type":"string"}}],"responses":{"200":{"description":"List of entity traits","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbEntityTrait"}}},"required":["data"]}}}},"404":{"description":"Entity not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/{entityId}/events":{"get":{"tags":["ackDB"],"summary":"Get activity timeline for an entity","description":"Returns the entity's activity timeline - events from all sources, ordered by timestamp. Filterable by source type and event type. Use for building activity feeds or investigating what happened with a customer.","operationId":"getEntityEvents","parameters":[{"name":"entityId","in":"path","required":true,"description":"Entity ID (ULID)","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Maximum number of events to return.","schema":{"type":"integer","default":20}},{"name":"offset","in":"query","required":false,"description":"Number of events to skip for pagination.","schema":{"type":"integer","default":0}},{"name":"source","in":"query","required":false,"description":"Filter by source type (e.g. `slack`, `stripe`).","schema":{"type":"string"}},{"name":"sources","in":"query","required":false,"description":"CSV include list — only events from these sources (e.g. `slack,stripe`). Mutually exclusive with `source` and `excludeSources` (400).","schema":{"type":"string"}},{"name":"excludeSources","in":"query","required":false,"description":"CSV exclude list — drop events from these sources (e.g. `system,custom,member` to hide ackDB bookkeeping events). Mutually exclusive with `source` and `sources` (400).","schema":{"type":"string"}},{"name":"eventType","in":"query","required":false,"description":"Filter by event type (e.g. `slack_message`, `stripe_charge.succeeded`).","schema":{"type":"string"}},{"name":"since","in":"query","required":false,"description":"Filter events after this ISO date.","schema":{"type":"string","format":"date-time"}},{"name":"until","in":"query","required":false,"description":"Filter events before this ISO date.","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"Paginated list of entity events","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbEntityEvent"}},"pagination":{"$ref":"#/components/schemas/AckdbPaginationMeta"}},"required":["data","pagination"]}}}},"400":{"description":"More than one of `source`, `sources`, `excludeSources` was sent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"Entity not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/full":{"get":{"tags":["ackDB"],"summary":"Get the full entity object by identity link","description":"Resolve an entity by an identity-link **value** (pass just `externalIdValue` - a domain, slack_channel_id, stripe_customer_id, email, …; `externalIdType` and `source` are optional disambiguators) and return the complete entity object in one response: entity, identity links, traits, contacts, plus paginated windows over events, raw payloads, conversations, and files. Each high-volume collection is independently tunable (`*Limit`/`*Offset` + filters) and returns latest-first with a `total`, so callers can page the long tail. A bare call returns a complete-but-bounded object; set a collection's `*Limit=0` to omit it. Differs from `/entities/{entityId}/profile` (keyed by entityId, last 10 events only).","operationId":"getEntityFull","parameters":[{"name":"externalIdValue","in":"query","required":true,"description":"The identifier to resolve - a domain, slack_channel_id, stripe_customer_id, email, etc. This value alone resolves the entity in the common case.","schema":{"type":"string"},"example":"acme.com"},{"name":"externalIdType","in":"query","required":false,"description":"OPTIONAL. Identity-link type. Not needed to resolve (the value is unique); pass it only to disambiguate a value registered under more than one type (otherwise a collision returns 409).","schema":{"type":"string"},"examples":{"domain":{"value":"domain","summary":"Universal - company domain"},"slack":{"value":"slack_channel_id","summary":"Source-specific - Slack channel"},"stripe":{"value":"stripe_customer_id","summary":"Source-specific - Stripe customer"}}},{"name":"source","in":"query","required":false,"description":"OPTIONAL. Source type (e.g. `slack`, `stripe`). An extra disambiguator only; never required to resolve.","schema":{"type":"string"},"example":"slack"},{"name":"eventsLimit","in":"query","required":false,"description":"Max events to return (0–500, default 50; 0 omits the collection).","schema":{"type":"integer","default":50}},{"name":"eventsOffset","in":"query","required":false,"description":"Event pagination offset (default 0).","schema":{"type":"integer","default":0}},{"name":"eventsSource","in":"query","required":false,"description":"Filter events by source (e.g. `slack`).","schema":{"type":"string"}},{"name":"eventsType","in":"query","required":false,"description":"Filter events by event type (e.g. `slack_message`).","schema":{"type":"string"}},{"name":"eventsSince","in":"query","required":false,"description":"Only events at/after this ISO timestamp.","schema":{"type":"string","format":"date-time"}},{"name":"eventsUntil","in":"query","required":false,"description":"Only events at/before this ISO timestamp.","schema":{"type":"string","format":"date-time"}},{"name":"rawLimit","in":"query","required":false,"description":"Max raw payloads (0–500, default 20; 0 omits).","schema":{"type":"integer","default":20}},{"name":"rawOffset","in":"query","required":false,"description":"Raw payload pagination offset (default 0).","schema":{"type":"integer","default":0}},{"name":"conversationsLimit","in":"query","required":false,"description":"Max conversations (0–200, default 20; 0 omits).","schema":{"type":"integer","default":20}},{"name":"conversationsOffset","in":"query","required":false,"description":"Conversation pagination offset (default 0).","schema":{"type":"integer","default":0}},{"name":"conversationsSource","in":"query","required":false,"description":"Filter conversations by source.","schema":{"type":"string"}},{"name":"conversationsTopic","in":"query","required":false,"description":"Filter conversations by topic.","schema":{"type":"string"}},{"name":"conversationsSentiment","in":"query","required":false,"description":"Filter conversations by sentiment.","schema":{"type":"string"}},{"name":"conversationsSince","in":"query","required":false,"description":"Only conversations at/after this ISO timestamp.","schema":{"type":"string","format":"date-time"}},{"name":"conversationsUntil","in":"query","required":false,"description":"Only conversations at/before this ISO timestamp.","schema":{"type":"string","format":"date-time"}},{"name":"filesLimit","in":"query","required":false,"description":"Max files (0–500, default 50; 0 omits).","schema":{"type":"integer","default":50}},{"name":"filesOffset","in":"query","required":false,"description":"File pagination offset (default 0).","schema":{"type":"integer","default":0}},{"name":"filesSource","in":"query","required":false,"description":"Filter files by source.","schema":{"type":"string"}},{"name":"filesType","in":"query","required":false,"description":"Filter files by file type.","schema":{"type":"string"}},{"name":"filesProcessed","in":"query","required":false,"description":"Filter by processed state (true/false).","schema":{"type":"boolean"}},{"name":"contacts","in":"query","required":false,"description":"Set `false` to omit the contacts list.","schema":{"type":"boolean","default":true}},{"name":"conversationSummary","in":"query","required":false,"description":"Set `false` to omit the conversation rollup.","schema":{"type":"boolean","default":true}}],"responses":{"200":{"description":"The full entity object. A collection key is present only when its `*Limit` > 0 (and `conversationSummary` only when threads exist).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"entity":{"$ref":"#/components/schemas/AckdbEntity"},"identityLinks":{"type":"array","items":{"$ref":"#/components/schemas/AckdbIdentityLink"}},"traits":{"type":"array","items":{"$ref":"#/components/schemas/AckdbEntityTrait"}},"contacts":{"type":"array","description":"Contacts for the entity (see GET /entities/{entityId}/contacts).","items":{"type":"object"}},"events":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbEntityEvent"}},"total":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"}},"required":["data","total","limit","offset"]},"rawPayloads":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbRawPayloadRecord"}},"total":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"}},"required":["data","total","limit","offset"]},"conversations":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbConversation"}},"total":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"}},"required":["data","total","limit","offset"]},"files":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbFileMetadata"}},"total":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"}},"required":["data","total","limit","offset"]},"conversationSummary":{"type":"object","description":"Conversation rollup (present only when the entity has threads).","properties":{"totalThreads":{"type":"integer"},"distinctTopics":{"type":"integer"},"totalMessages":{"type":"integer"},"topTopics":{"type":"array","items":{"type":"string"}},"overallSentiment":{"type":["string","null"]}}}},"required":["entity","identityLinks","traits"]},"resolvedBy":{"type":"object","description":"Echoes the value used plus the identity-link type/source it matched.","properties":{"externalIdValue":{"type":"string"},"externalIdType":{"type":"string"},"source":{"type":"string"}},"required":["externalIdValue","externalIdType","source"]}},"required":["data","resolvedBy"]}}}},"404":{"description":"No entity is linked to the given value","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","example":"ENTITY_NOT_FOUND"},"message":{"type":"string","example":"No entity linked to acme.com"}},"required":["error","message"]}}}},"409":{"description":"The value is registered under more than one identity-link type and maps to multiple entities. Re-request with `externalIdType` (and/or `source`) to disambiguate.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","example":"AMBIGUOUS_IDENTITY"},"message":{"type":"string","example":"\"12345\" matches 2 entities across identity-link types; pass externalIdType to disambiguate."},"candidates":{"type":"array","description":"The identity-link type/source pairs the value matched.","items":{"type":"object","properties":{"externalIdType":{"type":"string"},"source":{"type":"string"}},"required":["externalIdType","source"]}}},"required":["error","message","candidates"]}}}},"422":{"description":"Missing required identity parameters","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","example":"MISSING_PARAMS"},"message":{"type":"string","example":"Both externalIdType and externalIdValue are required"}},"required":["error","message"]}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/search":{"get":{"tags":["ackDB"],"summary":"Fuzzy search → the single closest company (hydrated like /full)","description":"Per-field fuzzy search returning the **single closest company**, fully hydrated exactly like `/entities/full` (entity, identity links, traits, contacts, plus paginated events / raw payloads / conversations / files) — plus a `matchedOn` block explaining the hit. The fuzzy sibling of `/entities/full` and the company analog of `/contacts/search`: send whichever field(s) you're searching — `q` (across name + all identity links), `name`, `domain`, `identity` (any identity-link value, optionally pinned with `identityType`); at least one required, all **ANDed**. Ranked case-insensitive **substring** per field (exact > prefix > contains, score = sum of per-field tiers) and returns exactly one company. No match → 404. Accepts the same collection controls as `/entities/full` (`eventsLimit`, `rawLimit`, `conversationsLimit`, `filesLimit` + their filters, `contacts=false`, `conversationSummary=false`, and `*Limit=0` to omit a collection).","operationId":"searchEntity","parameters":[{"name":"q","in":"query","required":false,"description":"Free text to match against the entity name + all identity-link values (domain, slack_channel_id, stripe_customer_id, …). Case-insensitive substring; the closest single company is returned.","schema":{"type":"string"},"example":"perplexity"},{"name":"name","in":"query","required":false,"description":"Match against the company name only.","schema":{"type":"string"},"example":"perplexity"},{"name":"domain","in":"query","required":false,"description":"Match against the company's domain identity links only.","schema":{"type":"string"},"example":"perplexity.ai"},{"name":"identity","in":"query","required":false,"description":"Match against any identity-link value (Stripe customer ID, Slack channel ID, CRM account ID, …). Combine with `identityType` to pin one link type.","schema":{"type":"string"},"example":"cus_QwErTy123"},{"name":"identityType","in":"query","required":false,"description":"Restrict `identity` to one identity-link type (e.g. `stripe_customer_id`, `slack_channel_id`, `crm_account_id`). Only meaningful together with `identity`.","schema":{"type":"string"},"example":"stripe_customer_id"},{"name":"contactLimit","in":"query","required":false,"description":"Cap on nested contacts (0–1000). Omit to return all (the /full default). When set, the response also includes `contactCount` (total active contacts) so callers know whether there are more.","schema":{"type":"integer"}}],"responses":{"200":{"description":"The closest company, hydrated like GET /entities/full, plus a matchedOn block.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","description":"Same shape as GET /entities/full `data`: entity, identityLinks, traits, contacts (+ contactCount when contactLimit is set), events, rawPayloads, conversations, files, conversationSummary.","additionalProperties":true},"matchedOn":{"type":"object","description":"Why this company was picked — the strongest-matching provided field. `field` is `name`/`identity_link` for `q`-only searches, or the param name (`name`/`domain`/`identity`) for per-field searches.","properties":{"field":{"type":"string","enum":["name","identity_link","domain","identity"]},"value":{"type":"string"},"tier":{"type":"string","enum":["exact","prefix","contains"]}},"required":["field","value","tier"]}},"required":["data","matchedOn"]}}}},"404":{"description":"No company matches the given fields."},"422":{"description":"None of `q`, `name`, `domain`, `identity` provided."}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/{entityId}/profile":{"get":{"tags":["ackDB"],"summary":"Get aggregated entity profile","description":"Returns a complete entity profile - the entity, its identity links, all traits, and recent events in a single response. This is the primary endpoint for AI agents (L1 query tier). Designed for fast, single-call retrieval of everything known about an entity.","operationId":"getEntityProfile","parameters":[{"name":"entityId","in":"path","required":true,"description":"Entity ID (ULID)","schema":{"type":"string"}}],"responses":{"200":{"description":"Aggregated entity profile","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbEntityProfile"}},"required":["data"]}}}},"404":{"description":"Entity not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"},"example":{"error":{"code":"ENTITY_NOT_FOUND","message":"Entity not found: 01HXYZ123456789ABCDEFGH"}}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/{entityId}/raw":{"get":{"tags":["ackDB"],"summary":"Get raw payloads for an entity","description":"Returns raw source payloads stored for an entity (L3 evidence tier). Use for deep evidence, citations, or debugging what data arrived from source systems.","operationId":"getEntityRawPayloads","parameters":[{"name":"entityId","in":"path","required":true,"description":"Entity ID (ULID)","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Maximum number of raw payloads to return.","schema":{"type":"integer","default":20}},{"name":"offset","in":"query","required":false,"description":"Number of raw payloads to skip for pagination.","schema":{"type":"integer","default":0}}],"responses":{"200":{"description":"Paginated list of raw payloads","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbRawPayloadRecord"}},"pagination":{"$ref":"#/components/schemas/AckdbPaginationMeta"}},"required":["data","pagination"]}}}},"404":{"description":"Entity not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/{entityId}/search":{"get":{"tags":["ackDB"],"summary":"Semantic search over entity content","description":"Hybrid search (pgvector cosine similarity + BM25 keyword search) over an entity's embedded content chunks. Uses Reciprocal Rank Fusion (RRF) to combine results.\n\n**Requires:** an embedding provider configured (`GEMINI_API_KEY` by default, or `OPENAI_API_KEY` as the fallback provider - see `EMBEDDING_PROVIDER`) and `npm run embed` to have processed payloads.\nReturns 501 if semantic search is not configured.","operationId":"searchEntityContent","parameters":[{"name":"entityId","in":"path","required":true,"description":"Entity ID (ULID)","schema":{"type":"string"}},{"name":"q","in":"query","required":true,"description":"Natural language search query (e.g., 'pricing discussions', 'payment failures')","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Maximum number of results to return.","schema":{"type":"integer","default":10}}],"responses":{"200":{"description":"Ranked search results","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"source":{"type":"string"},"text":{"type":"string"},"score":{"type":"number","description":"RRF relevance score"},"timestamp":{"type":"string","format":"date-time"},"metadata":{"type":"object"}}}},"query":{"type":"string"},"total":{"type":"integer"}},"required":["data","query","total"]}}}},"404":{"description":"Entity not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"422":{"description":"Missing query parameter","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"501":{"description":"Semantic search not configured (no embedding provider - set GEMINI_API_KEY by default, or OPENAI_API_KEY for the fallback provider)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/resolve":{"post":{"tags":["ackDB"],"summary":"Resolve unresolved content to an entity","description":"Moves an unresolved content record to an existing or new entity. Creates identity links, stores raw payload, writes traits to Postgres, and deletes from unresolved pool.\n\n**Conflict checking:** If an identity link already exists on a different entity than the requested one, returns 409 IDENTITY_CONFLICT. Caller must merge entities first, then resolve.","operationId":"resolveEntity","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbResolveRequest"},"example":{"unresolvedContentId":"01HXYZ123456789ABCDEFGH","entityId":"01HXYZ123456789ABCDEFGI"}}}},"responses":{"200":{"description":"Content resolved to entity successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbResolveResponse"},"example":{"data":{"entityId":"01HXYZ123456789ABCDEFGI","contentId":"01HXYZ123456789ABCDEFGJ","created":false,"identityLinksCreated":1,"traitsWritten":2}}}}},"404":{"description":"Unresolved content or entity not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"409":{"description":"Identity link already belongs to a different entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"},"example":{"error":{"code":"IDENTITY_CONFLICT","message":"Identity link slack_channel_id=C04ABC already belongs to entity 01HABC. Merge first, then resolve."}}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/merge":{"post":{"tags":["ackDB"],"summary":"Merge two duplicate entities","description":"Combines two entities into one using a winner/loser pattern. ALL data transfers from loser to winner: identity_links, traits, contacts, events, raw_payloads, entity_content, files, and conversations. No data is lost.\n\nThe loser entity is soft-deleted (status=merged, merged_into=winnerId).\n\nTrait conflicts resolved by Postgres - winner's existing traits take precedence on key conflict. Identity link conflicts are handled automatically (loser's duplicate links are removed).\n\n**Contacts (#325):** when the same PERSON has an active contact at BOTH companies, the two edges are the same affiliation split across duplicates — they are MERGED (events re-pointed, links/traits/ownership folded onto the surviving edge), reported as `contactsMerged`. Everything else is simply re-parented (`contactsTransferred`).","operationId":"mergeEntities","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbMergeRequest"},"example":{"winnerId":"01HXYZ123456789ABCDEFGH","loserId":"01HABCD123456789EFGHIJK"}}}},"responses":{"200":{"description":"Entities merged successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbMergeResponse"},"example":{"data":{"winnerId":"01HXYZ123456789ABCDEFGH","loserId":"01HABCD123456789EFGHIJK","linksTransferred":2,"traitsTransferred":3,"contactsTransferred":1,"eventsTransferred":42,"rawPayloadsTransferred":42,"contentTransferred":5,"filesTransferred":0,"conversationsTransferred":3,"contactsMerged":0,"memberAssignmentsTransferred":0,"relationshipsRepointed":2,"relationshipDuplicatesConsolidated":1,"selfRelationshipsRetracted":1}}}}},"404":{"description":"Entity not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/{ref}/links":{"get":{"tags":["ackDB"],"summary":"List a company's identity links","description":"All identity links for a company, addressed by `ref` — an entity id (ULID) OR a domain (e.g. `acme.com`). Pass `?type=` to filter to a single link type.","operationId":"listEntityLinks","parameters":[{"name":"ref","in":"path","required":true,"description":"Entity id (ULID) or domain.","schema":{"type":"string"},"example":"acme.com"},{"name":"type","in":"query","required":false,"description":"Filter to one link type (e.g. `slack_channel_id`).","schema":{"type":"string"}}],"responses":{"200":{"description":"The company's identity links (all, or filtered to `?type=`)","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbIdentityLink"}}},"required":["data"]}}}},"404":{"description":"No entity matches `ref`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/{ref}/links/{type}/{value}":{"delete":{"tags":["ackDB"],"summary":"Remove one identity link from a company (no source segment)","description":"Removes one `(type, value)` pair. No source segment in the path — `(type, value)` is globally unique across universal and source-declared types, so the source stamp is redundant here. `value` must be URL-encoded (e.g. a value containing `/`). `type` cannot be `domain`; use the legacy source-qualified DELETE for surgical domain fixes.","operationId":"deleteEntityLink","parameters":[{"name":"ref","in":"path","required":true,"description":"Entity id (ULID) or domain.","schema":{"type":"string"},"example":"acme.com"},{"name":"type","in":"path","required":true,"description":"The identity link type. Cannot be `domain`.","schema":{"type":"string"},"example":"duns_number"},{"name":"value","in":"path","required":true,"description":"The external id value. URL-encode it.","schema":{"type":"string"},"example":"123456789"}],"responses":{"204":{"description":"Identity link removed"},"404":{"description":"No entity matches `ref`, or no such link exists on it (`LINK_NOT_FOUND`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"},"example":{"error":{"code":"LINK_NOT_FOUND","message":"Identity link not found: duns_number=123456789 on entity 01HABC123"}}}}},"409":{"description":"`ref` resolves to a merged (non-active) entity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"422":{"description":"`type` is `domain`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"},"example":{"error":{"code":"SCHEMA_VALIDATION_ERROR","message":"\"domain\" links are managed by entity resolution (ingest, merge). For surgical fixes use the legacy source-qualified DELETE."}}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/{entityId}/links/{source}/{externalIdType}/{externalIdValue}":{"delete":{"tags":["ackDB"],"summary":"Unlink an identity from an entity","deprecated":true,"description":"**Deprecated** — prefer `DELETE /entities/{ref}/links/{type}/{value}` (no source segment; `(type, value)` is already globally unique). Kept for backward compatibility and for surgical fixes that must target one specific source stamp (e.g. a stray `domain` link written by an old ingest). Removes one specific identity link. Does NOT delete the entity or any content/traits. If the entity has zero remaining links after unlinking, it becomes an orphan (still queryable, just not resolvable from external IDs).","operationId":"unlinkIdentity","parameters":[{"name":"entityId","in":"path","required":true,"description":"Entity ID (ULID)","schema":{"type":"string"}},{"name":"source","in":"path","required":true,"description":"Source system (e.g. `slack`, `stripe`)","schema":{"type":"string"}},{"name":"externalIdType","in":"path","required":true,"description":"External ID type (e.g. `slack_channel_id`)","schema":{"type":"string"}},{"name":"externalIdValue","in":"path","required":true,"description":"External ID value (e.g. `C04ABC`)","schema":{"type":"string"}}],"responses":{"204":{"description":"Identity link removed successfully"},"404":{"description":"Identity link not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/{entityId}/recompute":{"post":{"tags":["ackDB"],"summary":"Trigger trait recomputation (placeholder)","description":"Placeholder endpoint for trait recomputation. In production, ackDB fires a webhook event and the caller (Floqer) handles recomputation by replaying relevant workflows. For POC, this is a no-op that acknowledges the request.","operationId":"recomputeTraits","parameters":[{"name":"entityId","in":"path","required":true,"description":"Entity ID (ULID)","schema":{"type":"string"}}],"responses":{"200":{"description":"Recomputation acknowledged","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"message":{"type":"string","example":"Recomputation triggered"},"entityId":{"type":"string","example":"01HXYZ123456789ABCDEFGH"}},"required":["message","entityId"]}},"required":["data"]}}}},"404":{"description":"Entity not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/{entityId}/conversations":{"get":{"tags":["ackDB"],"summary":"List or search entity conversations","description":"Returns grouped, LLM-summarized conversations for an entity. When 'q' param is provided, uses hybrid semantic + keyword search. Otherwise returns filtered paginated list. Conversations group raw messages by thread (Slack thread_ts, daily digest for standalone messages).","operationId":"getEntityConversations","parameters":[{"name":"entityId","in":"path","required":true,"schema":{"type":"string"}},{"name":"q","in":"query","description":"Search query for semantic search","schema":{"type":"string"}},{"name":"source","in":"query","description":"Filter by source","schema":{"type":"string"}},{"name":"topic","in":"query","description":"Filter by topic","schema":{"type":"string"}},{"name":"sentiment","in":"query","description":"Filter by sentiment","schema":{"type":"string","enum":["positive","negative","neutral","mixed"]}},{"name":"hasActionItems","in":"query","description":"Filter to conversations with action items","schema":{"type":"boolean"}},{"name":"since","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"until","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"limit","in":"query","schema":{"type":"integer","default":20}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}}],"responses":{"200":{"description":"Conversations list or search results","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbConversation"}},"pagination":{"type":"object","properties":{"total":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"}}}}}}}},"404":{"description":"Entity not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/config/internal-domains":{"get":{"tags":["ackDB"],"summary":"List internal domains","description":"Returns the configured internal email domains used for auto-classifying contacts.","responses":{"200":{"description":"Internal domains list","content":{"application/json":{"schema":{"type":"object","properties":{"domains":{"type":"array","items":{"type":"string"},"example":["floqer.com","floqer.io"]}}}}}}},"security":[{"apiKey":[]}]},"put":{"tags":["ackDB"],"summary":"Set internal domains","description":"Sets the internal email domains list. Contacts whose email domain matches any of these are auto-classified as internal (is_external=false) during ingest.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["domains"],"properties":{"domains":{"type":"array","items":{"type":"string"},"example":["floqer.com","floqer.io"]}}}}}},"responses":{"200":{"description":"Updated domains list (normalized: lowercase, deduplicated)","content":{"application/json":{"schema":{"type":"object","properties":{"domains":{"type":"array","items":{"type":"string"}}}}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/config/internal-domains/reclassify":{"post":{"tags":["ackDB"],"summary":"Bulk reclassify contacts","description":"Reclassifies all existing contacts against the current internal domains and internal users lists. Contacts whose email domain matches an internal domain (or whose email matches an internal user) are set to is_external=false. Contacts that no longer match are set back to is_external=true.","responses":{"200":{"description":"Number of contacts updated","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"integer","example":12}}}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/config/internal-users":{"get":{"tags":["ackDB"],"summary":"List internal users","description":"Returns all explicitly configured internal users.","responses":{"200":{"description":"Internal users list","content":{"application/json":{"schema":{"type":"object","properties":{"users":{"type":"array","items":{"$ref":"#/components/schemas/AckdbInternalUser"}}}}}}}},"security":[{"apiKey":[]}]},"post":{"tags":["ackDB"],"summary":"Add internal user","description":"Adds a known internal team member. Contacts matching this user's email or source IDs will be auto-classified as internal during ingest.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","example":"sarah@gmail.com"},"name":{"type":"string","example":"Sarah Chen"},"sourceIds":{"type":"array","items":{"type":"object","properties":{"source":{"type":"string"},"externalIdType":{"type":"string"},"externalIdValue":{"type":"string"}}}},"notes":{"type":"string","example":"Contractor, works on billing"}}}}}},"responses":{"201":{"description":"Created internal user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbInternalUser"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/config/internal-users/{id}":{"delete":{"tags":["ackDB"],"summary":"Remove internal user","description":"Removes an internal user. Future ingest will no longer classify this user as internal.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Internal user ID (ULID)"}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"}}}}}},"404":{"description":"Internal user not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/companies/fields":{"get":{"tags":["ackDB"],"summary":"List Observe company fields","description":"Returns the field catalog for company filtering/sorting/projection - system attributes, registered traits (scope=entity), and event types - each with coverage, legal operators, and value type.","operationId":"listObserveCompanyFields","responses":{"200":{"description":"The field catalog (may be coverage-ordered).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbFieldDescriptor"}}},"required":["data"]}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/companies/aggregatable-events":{"get":{"tags":["ackDB"],"summary":"Event-aggregate composer catalog","description":"Authoritative coverage-gated event/field vocabulary for the selected grain; unchanged response shape.","operationId":"listObserveAggregatableEvents","responses":{"200":{"description":"Sources -> event types -> aggregatable fields.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"sources":{"type":"array","items":{"type":"object","properties":{"source":{"type":"string"},"label":{"type":"string"},"events":{"type":"array","items":{"type":"object","properties":{"eventType":{"type":"string","description":"Bare event type."},"label":{"type":"string"},"fields":{"type":"array","items":{"$ref":"#/components/schemas/AckdbAggregatableFieldInfo"}}},"required":["eventType","label","fields"]}},"fields":{"type":"array","items":{"$ref":"#/components/schemas/AckdbAggregatableFieldInfo"}}},"required":["source","label","events","fields"]}}},"required":["sources"]}},"required":["data"]}}}}},"parameters":[{"name":"grain","in":"query","schema":{"type":"string","enum":["company","contact","person"],"default":"company"},"description":"Person includes personal and affiliated events; company/contact exclude sole-person events."}],"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/companies/fields/{key}/values":{"get":{"tags":["ackDB"],"summary":"List distinct values for a field","description":"Returns distinct values for a pickable field, most-common first, capped at the cardinality limit: a string/string_array trait, a whitelisted system attribute (e.g. status), or a string/string_array promoted event-metadata (emf__) column. Only fields whose descriptor carries distinctValuesUrl are pickable; everything else is rejected. A twin endpoint exists per grain (/observe/contacts/fields/{key}/values). Pass ?search= to filter the values by a case-insensitive substring - required for high-cardinality fields whose match isn't in the top results (the picker's typeahead uses this).","operationId":"listObserveFieldValues","parameters":[{"name":"key","in":"path","required":true,"description":"The field key (trait key, system attribute, or emf__ column key).","schema":{"type":"string"},"example":"hubspot_lifecycle_stage"},{"name":"search","in":"query","required":false,"description":"Case-insensitive substring filter over the distinct values. LIKE metacharacters are matched literally. Omitted/blank returns the unfiltered most-common values. Max 100 chars.","schema":{"type":"string","maxLength":100},"example":"enterprise"}],"responses":{"200":{"description":"Distinct values for the field.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"values":{"type":"array","items":{"type":"string"}},"cardinalityCapped":{"type":"boolean"}},"required":["values","cardinalityCapped"]}},"required":["data"]}}}},"400":{"description":"Field value type does not support distinct-value listing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"Unknown field key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/companies/fields/{key}/label":{"put":{"tags":["ackDB"],"summary":"Set a company field's display-name override","description":"Per-deployment override of a column's DISPLAY label in the company grain (#191). Presentation only - never touches the field key, the compiler whitelist, filters, sort, or ingest. Unknown keys are accepted (stored inert). Reflected on the next GET /observe/companies/fields. Use DELETE to reset to the computed name.","operationId":"setObserveCompanyFieldLabel","parameters":[{"name":"key","in":"path","required":true,"description":"The field key (FieldDescriptor.key) to relabel.","schema":{"type":"string"},"example":"measure_status"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string","example":"Customer Status"}},"required":["label"]}}}},"responses":{"200":{"description":"The stored override.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbFieldLabelOverride"}},"required":["data"]}}}},"400":{"description":"Empty or over-long label.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]},"delete":{"tags":["ackDB"],"summary":"Clear a company field's display-name override","description":"Remove the override (revert to the computed label). Idempotent - `deleted` is false if none existed.","operationId":"clearObserveCompanyFieldLabel","parameters":[{"name":"key","in":"path","required":true,"description":"The field key to reset.","schema":{"type":"string"},"example":"measure_status"}],"responses":{"200":{"description":"Whether an override row was removed.","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"}},"required":["deleted"]}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/contacts/fields/{key}/label":{"put":{"tags":["ackDB"],"summary":"Set a contact field's display-name override","description":"Per-deployment override of a column's DISPLAY label in the contact grain (#191). Presentation only - never touches the field key, the compiler whitelist, filters, sort, or ingest. Unknown keys are accepted (stored inert). Reflected on the next GET /observe/contacts/fields. Use DELETE to reset to the computed name.","operationId":"setObserveContactFieldLabel","parameters":[{"name":"key","in":"path","required":true,"description":"The field key (FieldDescriptor.key) to relabel.","schema":{"type":"string"},"example":"title"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string","example":"Job Title"}},"required":["label"]}}}},"responses":{"200":{"description":"The stored override.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbFieldLabelOverride"}},"required":["data"]}}}},"400":{"description":"Empty or over-long label.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]},"delete":{"tags":["ackDB"],"summary":"Clear a contact field's display-name override","description":"Remove the override (revert to the computed label). Idempotent - `deleted` is false if none existed.","operationId":"clearObserveContactFieldLabel","parameters":[{"name":"key","in":"path","required":true,"description":"The field key to reset.","schema":{"type":"string"},"example":"title"}],"responses":{"200":{"description":"Whether an override row was removed.","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"}},"required":["deleted"]}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/companies/query":{"post":{"tags":["ackDB"],"summary":"Query companies","description":"Compiles the filter/sort/columns into parameterized read-only SQL (run on the ackdb_readonly role) and returns a page of company rows with pagination metadata. If the count query is rejected or times out, `total` is clamped and `pagination.totalIsLowerBound` is set to true.","operationId":"queryObserveCompanies","requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbObserveQueryRequest"}}}},"responses":{"200":{"description":"A page of company rows.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbObserveQueryResponse"}}}},"400":{"description":"Invalid filter - unknown field, illegal operator, or rejected SQL (BAD_REQUEST).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/companies/export":{"post":{"tags":["ackDB"],"summary":"Export companies as CSV","description":"Exports the current filtered company view (or an explicit by-ids selection) as a CSV download. Body is the same ObserveQueryRequest the table queries with, EXCEPT `limit`/`offset` are ignored - the whole matched set is paged server-side. When `columns` is omitted it falls back to the company default-view columns. When `ids` is present (≤500), exactly those rows are exported (any `filter` is ANDed in); an empty `ids` array yields a header-only CSV. Cell values are RAW (currency as integer cents, dates as ISO-8601, sparkline/derived columns as JSON arrays); headers use catalog display labels and a leading `entityId` column is always included. A matched set larger than 10,000 rows is rejected with 400 - narrow the filter.","operationId":"exportObserveCompanies","requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbObserveQueryRequest"}}}},"responses":{"200":{"description":"A CSV document (UTF-8 with BOM, CRLF line endings).","headers":{"X-Export-Row-Count":{"description":"Number of data rows in the document (excludes the header row).","schema":{"type":"integer"}},"Content-Disposition":{"description":"Attachment with filename, e.g. `attachment; filename=\"companies-export.csv\"`.","schema":{"type":"string"}}},"content":{"text/csv":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Invalid filter/columns, too many ids (>500), or the matched set exceeds the 10,000-row export limit (BAD_REQUEST).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/contacts/export":{"post":{"tags":["ackDB"],"summary":"Export contacts as CSV","description":"Exports the current filtered contact view (or an explicit by-ids selection) as a CSV download. Identical contract to POST /observe/companies/export, on the contact grain: `ids` are `contactId`s, the omitted-`columns` fallback is the contact default view, and a leading `contactId` column is always included. `limit`/`offset` are ignored; the matched set is capped at 10,000 rows (over → 400) and `ids` at 500.","operationId":"exportObserveContacts","requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbObserveQueryRequest"}}}},"responses":{"200":{"description":"A CSV document (UTF-8 with BOM, CRLF line endings).","headers":{"X-Export-Row-Count":{"description":"Number of data rows in the document (excludes the header row).","schema":{"type":"integer"}},"Content-Disposition":{"description":"Attachment with filename, e.g. `attachment; filename=\"contacts-export.csv\"`.","schema":{"type":"string"}}},"content":{"text/csv":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Invalid filter/columns, too many ids (>500), or the matched set exceeds the 10,000-row export limit (BAD_REQUEST).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/companies/{entityId}/fields":{"patch":{"tags":["ackDB"],"summary":"Edit a company row's field values","description":"Directly edit the values a company row shows (#238) - a batch { \"<fieldKey>\": <value>, … }. The server routes each field by its catalog group: a TRAIT upserts entity_traits (op:set, the same row POST /ingest writes, stamped source=\"manual\", validated against the trait definition); the `name` attribute updates the entity. Computed/derived columns (activity, event_count, sources, last_active, revenue, credits, source_names, domain, created_at, …), event columns (append-only), and the internal `status` lifecycle enum are rejected per-field (edit the status/lifecycle TRAIT instead). Trait definitions, events, and raw_payloads are never touched. Partial success: valid fields apply, the rest return in `errors`.","operationId":"editObserveCompanyFields","parameters":[{"name":"entityId","in":"path","required":true,"description":"The entity_id of the company row to edit.","schema":{"type":"string"},"example":"01KWSVN7YY7Q2Q9QZ8GEHHYW14"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"description":"Field key → new value. Multiple fields allowed.","example":{"measure_status":"churned","name":"Acme Inc"}}}}},"responses":{"200":{"description":"The keys that were updated + a per-field error map for any that were rejected.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbObserveFieldEditResponse"}}}},"400":{"description":"Empty/non-object body, or no field could be updated (BAD_REQUEST; `details.errors` per field).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"No company with that entity_id (ENTITY_NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/contacts/{contactId}/fields":{"patch":{"tags":["ackDB"],"summary":"Edit a contact row's field values","description":"Contact-grain equivalent of PATCH /observe/companies/{entityId}/fields (#238). A TRAIT upserts contact_traits; whitelisted attributes (name, email, phone, title, department, linkedin_url, is_external) update the contact row. Computed/event/status fields are rejected per-field. Partial success; manual edits stamped source=\"manual\".","operationId":"editObserveContactFields","parameters":[{"name":"contactId","in":"path","required":true,"description":"The contact_id of the contact row to edit.","schema":{"type":"string"},"example":"01KWSVN7YY7Q2Q9QZ8GEHHYW14"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"description":"Field key → new value. Multiple fields allowed.","example":{"title":"VP Engineering","is_external":true}}}}},"responses":{"200":{"description":"The keys that were updated + a per-field error map for any that were rejected.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbObserveFieldEditResponse"}}}},"400":{"description":"Empty/non-object body, or no field could be updated (BAD_REQUEST; `details.errors` per field).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"No contact with that contact_id (ENTITY_NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/persons/fields":{"get":{"tags":["ackDB"],"summary":"List Observe person fields","description":"The field catalog for the PERSON grain (#329) - one row per human, spanning every company they have been at. Structurally different from the other two grains: a person holds durable KEYS and EVENTS, never traits, so the catalog ships NO trait descriptors (a trait filter is a 400). What it does ship: system columns (name, affiliation_count, last_active, is_internal, key_count, status, created_at, updated_at, plus the projection-only `companies` and `activity` columns), durable-key columns (`idl__<type>` - linkedin_url, email_address, person-scoped product ids), promoted event-metadata columns (`emf__…`, #212) and per-event-type count columns (`evc__…`, #322) - both correlated on the person's OWN timeline (entity_events.person_id), so they span every company the human has worked at. One field is deliberately NOT here: the company filter `entity_id` (#334). A human has 1..N companies, so it is a predicate (an EXISTS over their active edges), never a column - and a catalog entry would let a UI offer it as one and 400 the view. It is documented on POST /observe/persons/query, and the company grain's filter-only `source_present` is withheld from that grain's catalog for the same reason.","operationId":"listObservePersonFields","responses":{"200":{"description":"The field catalog (may be coverage-ordered).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbFieldDescriptor"}}},"required":["data"]}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/persons/fields/{key}/values":{"get":{"tags":["ackDB"],"summary":"List distinct values for a person field","description":"Person-grain twin of GET /observe/companies/fields/{key}/values, served by the same shared resolver (so the console and the agent's `get_field_values` tool cannot diverge). Pickable here: `name`, a durable-key column (`idl__<type>`), or a string/string_array promoted event-metadata (`emf__`) column - i.e. exactly the fields whose descriptor carries distinctValuesUrl. There are no person traits, so a trait key is a 400. Pass ?search= for the picker's case-insensitive substring typeahead.","operationId":"listObservePersonFieldValues","parameters":[{"name":"key","in":"path","required":true,"description":"The field key (system attribute, idl__ key column, or emf__ column key).","schema":{"type":"string"},"example":"idl__email_address"},{"name":"search","in":"query","required":false,"description":"Case-insensitive substring filter over the distinct values. LIKE metacharacters are matched literally. Omitted/blank returns the unfiltered most-common values. Max 100 chars.","schema":{"type":"string","maxLength":100},"example":"acme.com"}],"responses":{"200":{"description":"Distinct values for the field.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"values":{"type":"array","items":{"type":"string"}},"cardinalityCapped":{"type":"boolean"}},"required":["values","cardinalityCapped"]}},"required":["data"]}}}},"400":{"description":"Field has no value picker (no distinctValuesUrl), or is a trait key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"Unknown field key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/persons/fields/{key}/label":{"put":{"tags":["ackDB"],"summary":"Set a person field's display-name override","description":"Per-deployment override of a column's DISPLAY label in the person grain (#191). Presentation only - never touches the field key, the compiler whitelist, filters, sort, or ingest. Unknown keys are accepted (stored inert). Reflected on the next GET /observe/persons/fields. Use DELETE to reset to the computed name.","operationId":"setObservePersonFieldLabel","parameters":[{"name":"key","in":"path","required":true,"description":"The field key (FieldDescriptor.key) to relabel.","schema":{"type":"string"},"example":"affiliation_count"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string","example":"Companies worked at"}},"required":["label"]}}}},"responses":{"200":{"description":"The stored override.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbFieldLabelOverride"}},"required":["data"]}}}},"400":{"description":"Empty or over-long label.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]},"delete":{"tags":["ackDB"],"summary":"Clear a person field's display-name override","description":"Remove the override (revert to the computed label). Idempotent - `deleted` is false if none existed.","operationId":"clearObservePersonFieldLabel","parameters":[{"name":"key","in":"path","required":true,"description":"The field key to reset.","schema":{"type":"string"},"example":"affiliation_count"}],"responses":{"200":{"description":"Whether an override row was removed.","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"}},"required":["deleted"]}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/persons/query":{"post":{"tags":["ackDB"],"summary":"Query persons","description":"Person-grain twin of POST /observe/companies/query: compiles the filter/sort/columns into parameterized read-only SQL (ackdb_readonly role) and returns a page of person rows with pagination metadata. `ids` selects by `person_id`. Merged (tombstoned) persons never surface - the compiler's base gate is `status = 'active'`; the default sort is newest human first. Two filter node kinds are rejected with 400 on this grain: TRAIT conditions (a human has keys and events, not traits - filter their companies/contacts instead, or use an `idl__` key column) and raw EVENT conditions (use the `evc__`/`emf__` columns, which carry the STORED source-prefixed event type). COMPANY filter (#334): `{ \"type\": \"attribute\", \"field\": \"entity_id\", \"operator\": \"eq\"|\"in\"|\"is_null\"|\"is_not_null\", \"value\": … }` answers \"who works at X?\" - it compiles to an EXISTS over the person's ACTIVE affiliation edges, so someone who LEFT the company is NOT returned (they keep a `former` edge). It is FILTER-ONLY: a human has 1..N companies, so `entity_id` is not a column - project `companies` (a string_array of names) to display them, and note it is absent from GET /observe/persons/fields for that reason. `neq` and the ordered comparators are rejected rather than guessed at (on a 1..N edge set, \"not Acme\" is ambiguous). If the count query is rejected or times out, `total` is clamped and `pagination.totalIsLowerBound` is set to true.","operationId":"queryObservePersons","requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbObserveQueryRequest"}}}},"responses":{"200":{"description":"A page of person rows.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbObserveQueryResponse"}}}},"400":{"description":"Invalid filter - unknown field, illegal operator, a trait/event condition (unsupported on this grain), or rejected SQL (BAD_REQUEST).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/persons/export":{"post":{"tags":["ackDB"],"summary":"Export persons as CSV","description":"Exports the current filtered person view (or an explicit by-ids selection) as a CSV download. Identical contract to POST /observe/companies/export, on the person grain: `ids` are `personId`s, the omitted-`columns` fallback is the person default view, and a leading `personId` column is always included. `limit`/`offset` are ignored; the matched set is capped at 10,000 rows (over → 400) and `ids` at 500.","operationId":"exportObservePersons","requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbObserveQueryRequest"}}}},"responses":{"200":{"description":"A CSV document (UTF-8 with BOM, CRLF line endings).","headers":{"X-Export-Row-Count":{"description":"Number of data rows in the document (excludes the header row).","schema":{"type":"integer"}},"Content-Disposition":{"description":"Attachment with filename, e.g. `attachment; filename=\"persons-export.csv\"`.","schema":{"type":"string"}}},"content":{"text/csv":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Invalid filter/columns, too many ids (>500), or the matched set exceeds the 10,000-row export limit (BAD_REQUEST).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/persons/{personId}/fields":{"patch":{"tags":["ackDB"],"summary":"Edit a person row's field values","description":"Person-grain equivalent of PATCH /observe/companies/{entityId}/fields (#329) - a batch { \"<fieldKey>\": <value>, … }. Only TWO kinds of field are editable on a human: `name` (a direct persons.name UPDATE) and the durable-key columns (`idl__<type>`, replace-the-set through the same layer `POST /persons/{personId}/links` uses - values are normalized, role inboxes refused, and a value owned by ANOTHER human is REFUSED and recorded as a merge candidate, never re-pointed (first-writer-wins)). Everything else is rejected per-field: there are no person traits, the rollups (affiliation_count, key_count, is_internal, last_active, companies, activity, event columns) are computed, and `status` is the identity lifecycle set by the merge endpoints. Partial success: valid fields apply, the rest (including key conflicts) come back in `errors`. An `idl__email_address` add DECLINED as a fallback (#575 — the person already holds a `linkedin_url` anchor) that changed nothing else is reported in `errors` as a per-field notice, never counted in `updated`: email is a fallback anchor, and this grain has no edge to store it on.","operationId":"editObservePersonFields","parameters":[{"name":"personId","in":"path","required":true,"description":"The person_id of the person row to edit.","schema":{"type":"string"},"example":"01KWSVN7YY7Q2Q9QZ8GEHHYW14"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"description":"Field key → new value. Multiple fields allowed.","example":{"name":"Elly Chen","idl__email_address":["elly@acme.com","elly@globex.com"]}}}}},"responses":{"200":{"description":"The keys that were updated + a per-field error map for any that were rejected.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbObserveFieldEditResponse"}}}},"400":{"description":"Empty/non-object body, no field could be updated (`details.errors` per field), or the id is a MERGED (tombstoned) person - edit the survivor named in `details.mergedInto` (BAD_REQUEST).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"No person with that person_id (ENTITY_NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/segments":{"get":{"tags":["ackDB"],"summary":"List segments","description":"Returns the saved Segments the caller can see (soft-deleted excluded), most-recently-updated first. ACL-filtered to ones they own, are shared on, plus every legacy (owner-less) and org-default row; each carries the caller's effective `access` (#284). A service/static-key call sees all.","operationId":"listSegments","parameters":[{"name":"entityKind","in":"query","required":false,"description":"Filter by entity kind.","schema":{"type":"string","enum":["company","contact","person"]},"example":"company"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0}}],"responses":{"200":{"description":"List of segment summaries.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbSegmentSummary"}},"pagination":{"$ref":"#/components/schemas/AckdbPaginationMeta"}},"required":["data","pagination"]}}}}},"security":[{"apiKey":[]}]},"post":{"tags":["ackDB"],"summary":"Create a segment","description":"Creates a saved Segment from a name plus optional description, entityKind, filter, sort, and columns. The owner is stamped server-side from the caller's JWT (a client-supplied owner is ignored) and the segment is private to the owner by default (#284).","operationId":"createSegment","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","example":"Active paying companies"},"description":{"type":"string","example":"Companies with a paid Stripe invoice"},"entityKind":{"type":"string","enum":["company","contact","person"],"example":"company"},"filter":{"$ref":"#/components/schemas/AckdbObserveFilterGroup"},"sort":{"type":"array","items":{"$ref":"#/components/schemas/AckdbSegmentSort"}},"columns":{"type":"array","items":{"$ref":"#/components/schemas/AckdbSegmentColumn"}},"isDefault":{"type":"boolean","description":"Create as the default for its kind (clears any prior default). When true, name defaults to 'Default view' if omitted.","example":false},"baseSegmentId":{"type":"string","description":"Make this a DERIVED segment built on the given base segment: `filter` is the render-grain filter layered on top of the base's pivoted set. The base must exist and be a ROOT segment (no chains) - else 400. The render grain is an explicit `entityKind` when passed (must differ from the base's grain - else 400), otherwise the legacy company<->contact derivation. A base on the PERSON grain REQUIRES an explicit `entityKind` ('company' or 'contact') since either render grain is possible.","example":"01J9Z8Q3K2X7M4N5P6R7S8T9V0"}},"required":["name"]}}}},"responses":{"201":{"description":"Segment created.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbSegment"}},"required":["data"]}}}},"400":{"description":"Validation error - missing name.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/segments/default":{"get":{"tags":["ackDB"],"summary":"Get the default view for a kind","description":"Returns the default table view for a kind: a Segment explicitly marked default (source 'explicit') or the server-computed catalog fallback (source 'computed', id null). Always returns usable columns+sort so clients never hard-code the column policy.","operationId":"getDefaultSegment","parameters":[{"name":"entityKind","in":"query","required":false,"description":"Which kind's default to return (defaults to 'company').","schema":{"type":"string","enum":["company","contact","person"],"default":"company"},"example":"company"}],"responses":{"200":{"description":"The default view (explicit or computed).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbDefaultSegmentResponse"}},"required":["data"]}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/segments/{id}":{"get":{"tags":["ackDB"],"summary":"Get a segment","description":"Returns a single saved Segment by id, including the caller's effective `access` (#284). 403 if the caller cannot see it.","operationId":"getSegment","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"01J9Z8Q3K2X7M4N5P6R7S8T9V0"}],"responses":{"200":{"description":"The segment.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbSegment"}},"required":["data"]}}}},"403":{"description":"Caller cannot see the segment (#284).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"Segment not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]},"put":{"tags":["ackDB"],"summary":"Update a segment","description":"Updates a Segment in place (no version chain). Only the provided fields are changed. Requires edit access - owner, edit-grantee, or a legacy owner-less segment; 403 otherwise (#284).","operationId":"updateSegment","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"01J9Z8Q3K2X7M4N5P6R7S8T9V0"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"filter":{"$ref":"#/components/schemas/AckdbObserveFilterGroup"},"sort":{"type":"array","items":{"$ref":"#/components/schemas/AckdbSegmentSort"}},"columns":{"type":"array","items":{"$ref":"#/components/schemas/AckdbSegmentColumn"}}}}}}},"responses":{"200":{"description":"The updated segment.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbSegment"}},"required":["data"]}}}},"403":{"description":"Caller lacks edit access (#284).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"Segment not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]},"delete":{"tags":["ackDB"],"summary":"Delete a segment","description":"Soft-deletes a Segment (sets deleted_at). Owner only (#284).","operationId":"deleteSegment","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"01J9Z8Q3K2X7M4N5P6R7S8T9V0"}],"responses":{"200":{"description":"Segment soft-deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"deleted":{"type":"boolean","example":true}},"required":["deleted"]}},"required":["data"]}}}},"403":{"description":"Caller is not the segment owner (#284).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"Segment not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/segments/{id}/default":{"put":{"tags":["ackDB"],"summary":"Set a segment as the default","description":"Marks this Segment the default view for its entity_kind, transactionally clearing any prior default for that kind (at most one default per kind). Owner only; marking a private segment default exposes it read-only to everyone via the list ACL (#284).","operationId":"setDefaultSegment","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"01J9Z8Q3K2X7M4N5P6R7S8T9V0"}],"responses":{"200":{"description":"The segment, now default.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbSegment"}},"required":["data"]}}}},"403":{"description":"Caller is not the segment owner (#284).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"Segment not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]},"delete":{"tags":["ackDB"],"summary":"Unset a segment as the default","description":"Clears this Segment's default flag, reverting its kind to the computed fallback. Owner only (#284).","operationId":"unsetDefaultSegment","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"01J9Z8Q3K2X7M4N5P6R7S8T9V0"}],"responses":{"200":{"description":"The segment, no longer default.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbSegment"}},"required":["data"]}}}},"403":{"description":"Caller is not the segment owner (#284).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"Segment not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/segments/{id}/shares":{"get":{"tags":["ackDB"],"summary":"List a segment's member shares","description":"Returns the members a segment is shared with — memberId + name + email + role (#284). Visible to anyone who can see the segment; 403 otherwise.","operationId":"listSegmentShares","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"01J9Z8Q3K2X7M4N5P6R7S8T9V0"}],"responses":{"200":{"description":"The segment's grants, oldest first.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbSegmentShare"}}},"required":["data"]}}}},"403":{"description":"Caller cannot see the segment (#284).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"Segment not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]},"put":{"tags":["ackDB"],"summary":"Replace a segment's member shares","description":"Replaces the segment's ENTIRE grant set atomically (#284). Owner only. Each share is { memberId, role } — a member from the roster (GET /members) + 'view' or 'edit'. An unknown or inactive memberId → 400; duplicate members are deduped (last role wins).","operationId":"replaceSegmentShares","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"01J9Z8Q3K2X7M4N5P6R7S8T9V0"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"shares":{"type":"array","items":{"type":"object","properties":{"memberId":{"type":"string","example":"01J9Z8Q3K2X7M4N5P6R7S8T9V0"},"role":{"type":"string","enum":["view","edit"],"example":"edit"}},"required":["memberId","role"]}}},"required":["shares"]}}}},"responses":{"200":{"description":"The persisted grants, oldest first.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbSegmentShare"}}},"required":["data"]}}}},"400":{"description":"Invalid body (non-array, bad role, or an unknown/inactive memberId).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"403":{"description":"Caller is not the segment owner (#284).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"Segment not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/segments/{id}/results":{"get":{"tags":["ackDB"],"summary":"Get segment results","description":"Runs the saved Segment through the same compiler as the ad-hoc query endpoints and returns a page of rows for the rendered grain. By default the rows are the segment's HOME grain (`entityKind`). Pass `grain` to render the OTHER grain: the saved filter is pivoted via entity_id ('contacts at those companies' / 'companies of those people') and rendered with the TARGET grain's default columns/sort. Rows are the rendered grain's shape (company rows or contact rows) - read `renderedGrain` to dispatch the renderer. DERIVED segments (baseSegmentId set) render in their OWN grain only: the base's filter pivoted into this grain AND this segment's filter, with this segment's own columns/sort; `grain` != the segment's grain → 400. A transient `extraFilter` (e.g. search) and a transient `sort` override (header-click sorting) ARE honored on a derived segment; a `columns` override is not.","operationId":"getSegmentResults","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"01J9Z8Q3K2X7M4N5P6R7S8T9V0"},{"name":"grain","in":"query","required":false,"description":"Render grain. Omit for the segment's home grain (response is byte-identical to the pre-pivot shape). Set to ANOTHER grain to pivot the saved filter (company/contact membership travels via entity_id; the person grain bridges through contacts - a contact IS the person-company edge); the response then includes `renderedGrain` + `isPivot`. Every home-render pair among the three grains is supported.","schema":{"type":"string","enum":["company","contact","person"]}},{"name":"extraFilter","in":"query","required":false,"description":"URL-encoded JSON FilterGroup, authored in the RENDER (target) grain's vocabulary, ANDed on top of the pivoted set ('filter on top' - e.g. title contains VP on a company→contacts pivot, or a name/domain quick-find). Honored on a cross-grain pivot (grain present and ≠ the segment's home grain) AND on a derived segment (where it is ANDed with the derived segment's stored filter); passing it on the home-grain path is a 400 (use the filter editor / POST /query there). Unknown target-grain fields ⇒ 400.","schema":{"type":"string"}},{"name":"columns","in":"query","required":false,"description":"URL-encoded JSON array of column projections ([{ field, frozen?, width? }, …]) in the RENDER (target) grain's vocabulary, used to project the pivot rows instead of the target grain's default columns (the pivot column editor). Echoed back as `renderedColumns`. Honored ONLY on a cross-grain pivot; passing it on a derived segment or the home-grain path is a 400 (a derived segment renders its own stored columns; on the home grain use POST /query). Unknown target-grain fields ⇒ 400. The entity_id/name/domain base columns are always projected regardless, so row-click navigation is never dropped.","schema":{"type":"string"}},{"name":"sort","in":"query","required":false,"description":"URL-encoded JSON array of sort entries ([{ field, dir: 'asc'|'desc' }, …]) in the RENDER (target) grain's vocabulary — a TRANSIENT sort override (header-click sorting on a pivoted view). Honored on a cross-grain pivot (echoed back as `renderedSort`) AND on a derived segment (overrides the stored sort for this read only — edit + save to persist); passing it on the home-grain path is a 400. Unknown/unsortable target-grain fields ⇒ 400.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":1000,"default":50}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0}}],"responses":{"200":{"description":"A page of rows for the rendered grain. When `grain` is supplied, `renderedGrain` and `isPivot` are present (`isPivot` = renderedGrain differs from the segment's home grain). On a pivot, `renderedColumns` + `renderedSort` echo the target grain's default layout the rows were projected with.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/AckdbObserveQueryResponse"},{"type":"object","properties":{"renderedGrain":{"type":"string","enum":["company","contact","person"],"description":"The grain the rows belong to. Present only when `grain` was supplied."},"isPivot":{"type":"boolean","description":"True when renderedGrain differs from the segment's home grain."},"renderedColumns":{"type":"array","items":{"$ref":"#/components/schemas/AckdbSegmentColumn"},"description":"On a pivot (isPivot=true), the TARGET grain's default columns the rows were projected with - echo this to render the right layout (the rows are NOT the segment's stored, home-grain columns). Present only on a pivot."},"renderedSort":{"type":"array","items":{"$ref":"#/components/schemas/AckdbSegmentSort"},"description":"On a pivot, the sort the rows were actually ordered by - the TARGET grain's default, or the `sort` override when one was passed."}}}]}}}},"403":{"description":"Caller cannot see the segment (#284).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"Segment not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/segment-folders":{"get":{"tags":["ackDB"],"summary":"List the caller's segment folders","description":"Returns the caller's folders in rail order (#470). Folders are a PERSONAL filing layer — you only ever see your own — so every route here requires a signed-in user; a service/static-API-key call has no rail and gets 400. `entityKind` narrows the COUNTS only, never the folder list: a folder holding nothing in the current grain still renders empty, because hiding it would make folders vanish and reappear as you switch tabs.","operationId":"listSegmentFolders","parameters":[{"name":"entityKind","in":"query","required":false,"description":"Grain to scope each folder's `segmentCount` to. Omit to count every grain.","schema":{"type":"string","enum":["company","contact","person"]},"example":"company"}],"responses":{"200":{"description":"The caller's folders, ordered by `position`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbSegmentFolder"}}},"required":["data"]}}}},"400":{"description":"No user identity on the request (service / static-API-key call), or a bad entityKind.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]},"post":{"tags":["ackDB"],"summary":"Create a segment folder","description":"Creates a folder at the END of the caller's rail, so appending never reshuffles what they already arranged. Names are unique per owner, case-insensitively — two identically named folders in one rail are indistinguishable to the person reading it.","operationId":"createSegmentFolder","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":80,"example":"Q3 Enterprise Push"}},"required":["name"]}}}},"responses":{"201":{"description":"Folder created.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbSegmentFolder"}},"required":["data"]}}}},"400":{"description":"Missing/blank/too-long name, no user identity, or the 200-folder ceiling reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"409":{"description":"FOLDER_NAME_CONFLICT — the caller already has a folder with this name.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/segment-folders/reorder":{"put":{"tags":["ackDB"],"summary":"Reorder the caller's segment folders","description":"Rewrites every folder's `position` from the given order, in one transaction. `folderIds` must name the caller's COMPLETE set exactly once: a partial list has no defined meaning for the folders left out, and silently appending them produces a rail that reshuffles itself. On a mismatch nothing is reordered and the 400 body reports the difference under `error.details.errors.folderIds` as `{ missing, unknown }`.","operationId":"reorderSegmentFolders","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"folderIds":{"type":"array","items":{"type":"string"}}},"required":["folderIds"]}}}},"responses":{"200":{"description":"The caller's folders in their new order.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbSegmentFolder"}}},"required":["data"]}}}},"400":{"description":"folderIds is not the caller's complete folder set, or there is no user identity.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/segment-folders/{id}":{"put":{"tags":["ackDB"],"summary":"Rename a segment folder","operationId":"renameSegmentFolder","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"01JA0B1C2D3E4F5G6H7J8K9M0N"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":80,"example":"Q4 Enterprise Push"}},"required":["name"]}}}},"responses":{"200":{"description":"Folder renamed.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbSegmentFolder"}},"required":["data"]}}}},"404":{"description":"FOLDER_NOT_FOUND — no such folder, or it belongs to another user (the two are deliberately indistinguishable so a folder's existence cannot be probed by id).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"409":{"description":"FOLDER_NAME_CONFLICT — the caller already has a folder with this name.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]},"delete":{"tags":["ackDB"],"summary":"Delete a segment folder","description":"Deletes the folder. Its segments are UNFILED, never deleted — a folder is a view over segments, so discarding the view must not discard them. `unfiled` reports how many went back to Unfiled.","operationId":"deleteSegmentFolder","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"01JA0B1C2D3E4F5G6H7J8K9M0N"}],"responses":{"200":{"description":"Folder deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"deleted":{"type":"boolean","example":true},"unfiled":{"type":"integer","description":"Segments returned to Unfiled.","example":4}},"required":["deleted","unfiled"]}},"required":["data"]}}}},"404":{"description":"FOLDER_NOT_FOUND — no such folder, or it belongs to another user.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/segment-folders/{id}/items":{"put":{"tags":["ackDB"],"summary":"Replace a folder's contents (move + reorder)","description":"Replaces the folder's ordered contents. This is BOTH the move and the reorder call: array order becomes each segment's `position`, and filing a segment here pulls it out of whatever folder it was in (a segment sits in at most one folder per user), so a drag from folder A to folder B is ONE request against B. Segments previously in this folder and absent from the body are unfiled; segments the caller filed in OTHER folders and did not mention are untouched. Every id must be a segment the caller can SEE — `view` access is enough, since organising your own rail is not editing the segment. Unknown and invisible ids are reported together in one 400 (`error.details.errors.segmentIds`) so the response cannot be used to discover whether a private segment exists; nothing is moved.","operationId":"replaceSegmentFolderItems","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"01JA0B1C2D3E4F5G6H7J8K9M0N"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"segmentIds":{"type":"array","maxItems":500,"items":{"type":"string"},"description":"Segment ids in the desired order. Empty array empties the folder."}},"required":["segmentIds"]}}}},"responses":{"200":{"description":"The folder's contents after the write.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"folderId":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/components/schemas/AckdbSegmentFolderItem"}}},"required":["folderId","items"]}},"required":["data"]}}}},"400":{"description":"Malformed segmentIds, an id the caller cannot see, or no user identity.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"FOLDER_NOT_FOUND — no such folder, or it belongs to another user.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/segment-folders/items/{segmentId}":{"delete":{"tags":["ackDB"],"summary":"Unfile a segment","description":"Removes one segment from whatever folder the caller filed it in, returning it to Unfiled. Idempotent — unfiling an already-unfiled segment is the state the caller asked for, so it returns 200 with `unfiled:false` rather than 404. The segment itself is untouched.","operationId":"unfileSegment","parameters":[{"name":"segmentId","in":"path","required":true,"schema":{"type":"string"},"example":"01J9Z8Q3K2X7M4N5P6R7S8T9V0"}],"responses":{"200":{"description":"The segment is not in any folder.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"unfiled":{"type":"boolean","description":"True when a placement was actually removed; false when it was already unfiled.","example":true}},"required":["unfiled"]}},"required":["data"]}}}},"400":{"description":"No user identity on the request (service / static-API-key call).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/segments/{id}/export":{"get":{"tags":["ackDB"],"summary":"Export segment results as CSV","description":"Exports a saved Segment as a CSV download, resolving rows the same way as GET /observe/segments/{id}/results: the segment's HOME grain by default, a cross-grain pivot when `grain` is the other grain, or a DERIVED segment's own grain. The optional `extraFilter` / `columns` / `sort` params are pivot/derived-only (400 on the home grain), exactly as on /results. `limit`/`offset` do not apply - the whole matched set is paged server-side and capped at 10,000 rows (over → 400). Cell values are RAW; headers use catalog display labels with a leading identity column (`entityId`/`contactId`).","operationId":"exportSegmentResults","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"01J9Z8Q3K2X7M4N5P6R7S8T9V0"},{"name":"grain","in":"query","required":false,"description":"Render grain. Omit for the segment's home grain; set to another grain to pivot.","schema":{"type":"string","enum":["company","contact","person"]}},{"name":"extraFilter","in":"query","required":false,"description":"URL-encoded JSON FilterGroup 'filter on top' (pivot/derived only; 400 on the home grain).","schema":{"type":"string"}},{"name":"columns","in":"query","required":false,"description":"URL-encoded JSON SegmentColumn[] projection override (cross-grain pivot only).","schema":{"type":"string"}},{"name":"sort","in":"query","required":false,"description":"URL-encoded JSON SegmentSort[] transient sort override (pivot/derived only; 400 on the home grain) - keeps the CSV row order matching a header-sorted pivot view.","schema":{"type":"string"}}],"responses":{"200":{"description":"A CSV document (UTF-8 with BOM, CRLF line endings).","headers":{"X-Export-Row-Count":{"description":"Number of data rows in the document (excludes the header row).","schema":{"type":"integer"}},"Content-Disposition":{"description":"Attachment with filename derived from the segment name, e.g. `attachment; filename=\"My-segment.csv\"`.","schema":{"type":"string"}}},"content":{"text/csv":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Invalid grain/extraFilter/columns/sort, or the matched set exceeds the 10,000-row export limit (BAD_REQUEST).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"403":{"description":"Caller cannot see the segment (#284).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"Segment not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/{id}/logo":{"get":{"tags":["ackDB"],"summary":"Get a company logo (public)","description":"Streams the entity's logo image bytes for an <img> tag. PUBLIC - no auth (an <img> cannot send a bearer header, and company logos are public branding). 404 when the entity has no logo. `Cache-Control: public, max-age=86400`.","operationId":"getEntityLogo","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Entity id (ULID)."}],"responses":{"200":{"description":"The logo image (image/png | image/jpeg | image/gif | image/webp)."},"404":{"description":"Entity has no logo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}}},"put":{"tags":["ackDB"],"summary":"Set a company logo (by entity id)","description":"Uploads a logo for an existing entity: bytes go to the file store, a pointer is written to the entity row. Body is a base64-encoded image (png/jpeg/gif/webp, <= 2MB decoded). Re-uploading overwrites in place.","operationId":"setEntityLogo","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Entity id (ULID)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbEntityLogoUpload"}}}},"responses":{"200":{"description":"Logo stored.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"400":{"description":"Missing/invalid body, unsupported content type, or over the size cap.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"Entity not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/by-domain/{domain}/logo":{"put":{"tags":["ackDB"],"summary":"Set a company logo (by domain)","description":"Like PUT /entities/{id}/logo but resolves the entity from its `domain` identity link - the convenience surface for a domain-keyed enrichment pipeline. 404 when no entity has that domain.","operationId":"setEntityLogoByDomain","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"description":"The entity's domain (e.g. acme.com)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbEntityLogoUpload"}}}},"responses":{"200":{"description":"Logo stored.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"400":{"description":"Missing/invalid body, unsupported content type, or over the size cap.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"No entity with that domain.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/segments/{id}/changes":{"post":{"tags":["ackDB"],"summary":"Segment change feed (poll)","description":"Segment Sync S1: the change-log rows for a segment since cursor `N` - who entered / exited / re-entered. The response is ALWAYS a read of the committed append-only log (never an in-memory diff), so a poller that crashes before saving its cursor re-polls the same `since` and re-reads identical rows. The FIRST call bootstraps the snapshot and returns no rows (seed the initial members via /member-ids, so you never double-count). Runs under a non-blocking advisory lock (a concurrent call → 409). `since > maxSeq` → a 200 with `reconcile:true, reason:cursor_ahead`; `since` behind the prune watermark → 410 CURSOR_EXPIRED - both mean reconcile via /member-ids, never a silent empty-success. Derived segments are supported (their `filterHash` is the composite `own+base` - a base-filter edit flips it too); a cross-grain `?grain=` request → 400 (save the pivot as a derived segment to sync it); the sync tables not yet provisioned → 503. Poll again with `since = nextSince`. **Delivery is at-least-once** (a crash-safe re-poll re-reads the same rows; a /member-ids-seeded member can also reappear as an early enter) - dedup per member id.","operationId":"getSegmentChanges","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"01J9Z8Q3K2X7M4N5P6R7S8T9V0"},{"name":"since","in":"query","required":false,"description":"Cursor - return rows with seq > since. Omit or 0 to read from the earliest retained row.","schema":{"type":"integer","minimum":0},"example":0}],"responses":{"200":{"description":"A page of change rows (empty after bootstrap, or with a reconcile signal).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSegmentChangesResponse"}}}},"400":{"description":"Cross-grain `?grain=` request, a stale filter, or a malformed `since`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"No segment with that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"409":{"description":"LOCK_BUSY - another changes call holds this segment's lock; retry shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"410":{"description":"CURSOR_EXPIRED - `since` is behind the prune watermark; reconcile via /member-ids.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"503":{"description":"SYNC_TABLES_UNAVAILABLE - the segment-sync migration has not run on this tenant yet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/segments/{id}/member-ids":{"get":{"tags":["ackDB"],"summary":"Segment live member ids (keyset)","description":"Segment Sync S1: keyset-paginated full list of a segment's CURRENT members (the live filter, not the snapshot) - used to seed a delivery ledger and to reconcile an invalid cursor. The member id is the company `entity_id` (company segment) or the `contact_id` (contact segment). Derived segments compose with their base (base membership pivoted into the render grain AND the derived filter). Each page echoes `filterHash`; a hash change between pages means the population shifted mid-seed - restart. Page with `after = nextCursor` until `hasMore` is false. A cross-grain `?grain=` request → 400. Any query parameter other than `after`, `limit`, `grain` → 400 UNKNOWN_QUERY_PARAM (a pager sending `?cursor=` would otherwise get page 1 forever).","operationId":"getSegmentMemberIds","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"01J9Z8Q3K2X7M4N5P6R7S8T9V0"},{"name":"after","in":"query","required":false,"description":"Keyset cursor - return member ids strictly greater than this. Omit for the first page.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Page size (default 1000, max 5000).","schema":{"type":"integer","minimum":1,"maximum":5000}}],"responses":{"200":{"description":"A keyset page of member ids.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbMemberIdsResponse"}}}},"400":{"description":"Cross-grain `?grain=` request, an unknown query parameter, or a stale filter.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"No segment with that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/members":{"get":{"tags":["ackDB"],"summary":"List members (the internal GTM team)","description":"Members newest first. Pass ?activeOnly=true to exclude deactivated members.","operationId":"listMembers","parameters":[{"name":"activeOnly","in":"query","required":false,"schema":{"type":"boolean"},"description":"Exclude deactivated members."}],"responses":{"200":{"description":"A list of members.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}}},"security":[{"apiKey":[]}]},"post":{"tags":["ackDB"],"summary":"Create a member","description":"Idempotent on email — a repeat create returns the existing member (never a duplicate) and never clobbers its fields. Requires at least an email, a name, or sourceIds.","operationId":"createMember","requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbMemberCreateRequest"}}}},"responses":{"200":{"description":"The existing member (email already present).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"201":{"description":"The created member.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"400":{"description":"Invalid body (no email/name/sourceIds).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/members/{id}":{"get":{"tags":["ackDB"],"summary":"Get a member","operationId":"getMember","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Member id (ULID = internal_users.id)."}],"responses":{"200":{"description":"The member.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"404":{"description":"Member not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]},"patch":{"tags":["ackDB"],"summary":"Update a member (patch in place)","description":"Only provided fields change. title/team are free-text; managerId is the manager's member id.","operationId":"updateMember","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Member id (ULID = internal_users.id)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbMemberUpdateRequest"}}}},"responses":{"200":{"description":"The updated member.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"404":{"description":"Member not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/members/{id}/accounts":{"get":{"tags":["ackDB"],"summary":"List a member's accounts (their book of business)","description":"Paginated list of the active companies a member owns or handles, each with its assignment.","operationId":"getMemberAccounts","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Member id (ULID = internal_users.id)."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer"},"description":"Max items (default 50, max 200)."},{"name":"offset","in":"query","required":false,"schema":{"type":"integer"},"description":"Pagination offset."}],"responses":{"200":{"description":"A page of { entity, assignment } records + pagination.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"404":{"description":"Member not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/members/{id}/contacts":{"get":{"tags":["ackDB"],"summary":"List a member's contacts (their relationship book)","description":"Paginated list of the active contacts a member owns or handles (relationship ownership, #253), each with its assignment. Distinct from /accounts (companies).","operationId":"getMemberContacts","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Member id (ULID = internal_users.id)."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer"},"description":"Max items (default 50, max 200)."},{"name":"offset","in":"query","required":false,"schema":{"type":"integer"},"description":"Pagination offset."}],"responses":{"200":{"description":"A page of { contact, assignment } records + pagination.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"404":{"description":"Member not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/members/{id}/assignments":{"post":{"tags":["ackDB"],"summary":"Bulk-assign a member to many targets","description":"Assign one member to many companies (entityIds/domains) and/or contacts (contactIds) in one call. isPrimary applies to every target. Best-effort: returns { assigned, failed } (200 even on partial failure).","operationId":"bulkAssignMember","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Member id (ULID = internal_users.id)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbBulkAssignRequest"}}}},"responses":{"200":{"description":"{ assigned:[…], failed:[…] }.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"400":{"description":"No targets supplied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"Member not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]},"delete":{"tags":["ackDB"],"summary":"Bulk-unassign a member from many targets","description":"Soft-remove a member's active assignments across the listed companies (entityIds/domains) and/or contacts (contactIds). Idempotent per target.","operationId":"bulkUnassignMember","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Member id (ULID = internal_users.id)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbBulkUnassignRequest"}}}},"responses":{"200":{"description":"{ unassigned:[…] }.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"400":{"description":"No targets supplied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"Member not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/members/{id}/transfer":{"post":{"tags":["ackDB"],"summary":"Transfer a member's book to another member","description":"Hand off a departing member's active assignments (companies + contacts) to another member — the whole book (all:true) or a listed subset. Preserves each is_primary flag and emits ownership_transferred per owned account. Destination by toMemberId, else toEmail (resolves-or-creates).","operationId":"transferMemberBook","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Member id (ULID = internal_users.id)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbTransferBookRequest"}}}},"responses":{"200":{"description":"{ fromMemberId, toMemberId, companies, contacts }.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"400":{"description":"Missing destination, no targets, or self-transfer.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"Member not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/contacts/search":{"get":{"tags":["ackDB"],"summary":"Fuzzy contact search → the single closest contact (cross-entity)","description":"The contact analog of `/entities/search`. Pick a field and send a value — per-field params (all ANDed): `q` (across all fields), `name`, `email`, `phone`, `title`, `department`, `linkedin`; at least one required. Ranked case-insensitive substring (exact > prefix > contains) selects the single closest active contact across all companies by default. Set `includeFormer=true` to include ended affiliations too; active wins an equal-score tie and disabled contacts never qualify. Returns the contact hydrated with its identity links, traits, and a compact company ref (`entity: { entityId, name, domain }`), plus a sibling `matchedOn` block. No match → 404.","operationId":"searchContact","parameters":[{"name":"q","in":"query","required":false,"description":"Match across all searchable fields (name/email/phone/title/department/linkedin).","schema":{"type":"string"}},{"name":"name","in":"query","required":false,"description":"Match the contact name.","schema":{"type":"string"},"example":"Jane Doe"},{"name":"email","in":"query","required":false,"description":"Match the contact email.","schema":{"type":"string"},"example":"jane@acme.com"},{"name":"phone","in":"query","required":false,"description":"Match the contact phone.","schema":{"type":"string"}},{"name":"title","in":"query","required":false,"description":"Match the job title.","schema":{"type":"string"}},{"name":"department","in":"query","required":false,"description":"Match the department.","schema":{"type":"string"}},{"name":"linkedin","in":"query","required":false,"description":"Match the LinkedIn URL.","schema":{"type":"string"}},{"name":"includeFormer","in":"query","required":false,"description":"Include former (ended) affiliations in addition to active contacts. Disabled contacts remain excluded; active wins an equal-score tie. Defaults to false.","schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"The closest contact (incl. `personId`) + its identityLinks, traits, a compact company `entity` ref, and a `person` block naming the human plus their OTHER affiliations (`otherAffiliationCount`, `otherAffiliations[]` — including ended stints: a job change is the signal), alongside a sibling `matchedOn` { field, value, tier }.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"400":{"description":"includeFormer was supplied with a value other than \"true\" or \"false\".","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"No contact matches the given fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"422":{"description":"No field param provided (need ≥1 of q/name/email/phone/title/department/linkedin).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/contacts/{contactId}/links":{"get":{"tags":["ackDB"],"summary":"List a contact's identity links","description":"All identity links for one contact. Pass ?type= to filter to a single link type.","operationId":"listContactLinks","parameters":[{"name":"contactId","in":"path","required":true,"schema":{"type":"string"},"description":"Contact id (ULID = contacts.contact_id)."},{"name":"type","in":"query","required":false,"schema":{"type":"string"},"description":"Filter to one link type."}],"responses":{"200":{"description":"The contact's identity links (ContactIdentityLink[]: contactId, source, externalIdType, externalIdValue, confidence, linkedAt), all or filtered to ?type=.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"404":{"description":"No contact matches contactId.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/contacts/{contactId}/links/{type}/{value}":{"delete":{"tags":["ackDB"],"summary":"Remove one identity link from a contact","description":"Removes one (type, value) pair, regardless of its source stamp — works for any existing type, including legacy free-form types that were never registered (registration only gates ADDING, never removing). value must be URL-encoded.","operationId":"deleteContactLink","parameters":[{"name":"contactId","in":"path","required":true,"schema":{"type":"string"},"description":"Contact id (ULID = contacts.contact_id)."},{"name":"type","in":"path","required":true,"schema":{"type":"string"},"description":"The identity link type."},{"name":"value","in":"path","required":true,"schema":{"type":"string"},"description":"The external id value. URL-encode it."}],"responses":{"204":{"description":"Identity link removed."},"404":{"description":"No contact matches contactId, or no such link exists on it (LINK_NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"409":{"description":"The contact is disabled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/contacts/{id}/owner":{"get":{"tags":["ackDB"],"summary":"Get a contact's relationship owner","description":"The active primary relationship owner (MemberAssignment + Member) of a contact, or data:null when unowned. Relationship ownership is distinct from the company's account owner.","operationId":"getContactOwner","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Contact id (ULID = contacts.contact_id)."}],"responses":{"200":{"description":"The owner + member, or null.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"404":{"description":"Contact not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/contacts/{id}/members":{"get":{"tags":["ackDB"],"summary":"Get a contact's relationship team","description":"All active members who own/handle the relationship with the contact, owner first.","operationId":"getContactMembers","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Contact id (ULID = contacts.contact_id)."}],"responses":{"200":{"description":"The relationship team.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"404":{"description":"Contact not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/{id}/owner":{"get":{"tags":["ackDB"],"summary":"Get a company's primary owner (DRI)","description":"The active primary MemberAssignment + Member, or data:null when the account is unowned.","operationId":"getEntityOwner","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Company reference — an entity id (ULID) or a domain (e.g. acme.com)."}],"responses":{"200":{"description":"The owner + member, or null.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"404":{"description":"Company not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/{id}/members":{"get":{"tags":["ackDB"],"summary":"Get a company's account team","description":"All active members assigned to the company, owner first.","operationId":"getEntityMembers","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Company reference — an entity id (ULID) or a domain (e.g. acme.com)."}],"responses":{"200":{"description":"The account team.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"404":{"description":"Company not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/attributes":{"post":{"tags":["ackDB"],"summary":"Create a custom attribute definition","description":"Strict create — never an upsert; 409 if the key already exists (as an attribute OR a trait), collides with a reserved field for the scope, or starts with a registered source's `<source>_` prefix.","operationId":"createAttribute","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbAttributeCreateRequest"}}}},"responses":{"201":{"description":"The created attribute definition.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"400":{"description":"VALIDATION_ERROR — missing key/scope/type, bad key format (^[a-z][a-z0-9_]{0,63}$), unknown scope/type, or invalid allowedValues (non-string type, empty, or non-string items).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"409":{"description":"ATTRIBUTE_CONFLICT — key already exists (attribute or trait, message names the owner), is a reserved field for the scope, or uses a registered source's `<source>_` prefix.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]},"get":{"tags":["ackDB"],"summary":"List attribute definitions (standard + custom)","operationId":"listAttributes","parameters":[{"name":"scope","in":"query","required":false,"schema":{"type":"string","enum":["company","contact","person"]},"description":"Filter by grain."}],"responses":{"200":{"description":"Attribute definitions, each with `valueCount` (active records holding a value).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"400":{"description":"scope must be \"company\", \"contact\", or \"person\".","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/attributes/{key}":{"get":{"tags":["ackDB"],"summary":"Get one attribute definition","operationId":"getAttribute","parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string"},"description":"Attribute key (snake_case)."}],"responses":{"200":{"description":"The attribute definition.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"404":{"description":"ATTRIBUTE_NOT_FOUND — unknown key, or the key is a trait, not an attribute (message names the owning source).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]},"patch":{"tags":["ackDB"],"summary":"Edit an attribute's metadata, or migrate its scope","description":"Custom attributes: `key`/`type` are immutable — label/description/allowedValues change freely.\n\n**Standard attributes (#487):** `allowedValues` — and nothing else — may be edited, and only on an attribute that already ships an enum. A lifecycle funnel differs per deployment (an extra `\"Implementation\"` stage between `Opportunity` and `Customer`, say), so the ENUM belongs to the tenant while ackDB keeps the wording, grain and type. `null` is refused (the field must keep a fixed set of values), a freeform standard field stays freeform, and DELETE is still refused. The edit STICKS: it sets `userModified`, so the boot loader stops re-asserting the shipped list on restart. Every attribute view carries `editableFields` — the authoritative list of what PATCH will accept.\n\n**Scope migration (#474):** `{ \"scope\": \"person\" }` re-files a CONTACT attribute onto the HUMAN, in one transaction: values fold onto each edge's person (winner = most recently updated among ACTIVE edges; existing person values are never overwritten; disagreeing edges are reported as `conflicts[]`), the definition + label override flip scope, the migrated contact rows are deleted, saved segments referencing the key are SCRUBBED (it leaves the contact catalog — a referencing segment would 400 its view; affiliation-`where` references on company/person segments too), and score definitions referencing it are pruned + flagged `degraded`. Must be the ONLY field in the request. `{ \"dryRun\": true }` runs the full cascade and rolls back — the report is real. contact → person is the only direction. The CLI twin is `npm run migrate-contact-attributes`.","operationId":"updateAttribute","parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string"},"description":"Attribute key (snake_case)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbAttributePatchRequest"}}}},"responses":{"200":{"description":"The updated attribute definition — or, for a scope migration, `{ data: { contactValues, personValues, conflicts[], scrubbedSegments, degradedScores[], dryRun } }`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"400":{"description":"VALIDATION_ERROR — no fields provided, allowedValues invalid (including `null` on a standard attribute), a scope migration mixed with other fields, an unsupported scope direction, or the attribute is not contact-scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"403":{"description":"ATTRIBUTE_PROTECTED — a field this attribute does not expose: label/description/scope on a standard attribute, or allowedValues on a standard attribute that is freeform by design. The message names what IS editable (see `editableFields`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"ATTRIBUTE_NOT_FOUND.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"409":{"description":"ATTRIBUTE_VALUES_IN_USE — shrinking allowedValues would drop a value records still hold; details: [{ value, count }]. Clear or re-map those values first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]},"delete":{"tags":["ackDB"],"summary":"Delete a custom attribute","description":"Custom attributes only. Atomically removes the definition, ALL its value rows, segment-filter references (scrubbed, not bricked), and its label override.","operationId":"deleteAttribute","parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string"},"description":"Attribute key (snake_case)."}],"responses":{"200":{"description":"{ key, deleted, valuesDeleted, scrubbedSegmentCount }.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"403":{"description":"ATTRIBUTE_PROTECTED — standard attributes are shipped by ackDB and cannot be deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"ATTRIBUTE_NOT_FOUND.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/{id}/attributes":{"put":{"tags":["ackDB"],"summary":"Set/clear attribute values on a company","description":"`:id` = entity id or domain (identity-map resolved). All-or-nothing: every key/value is validated before anything is written. `null` clears a value — DELETEs the row, not a null-column write. Every REAL change emits a `trait.updated` event on the company timeline (actor = `actor` in the body, default `\"api\"`); a no-op write (value unchanged) writes and emits nothing.","operationId":"putEntityAttributes","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Company reference — an entity id (ULID) or a domain (e.g. acme.com)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbAttributePutValuesRequest"}}}},"responses":{"200":{"description":"{ entityId, applied: {key: value}, cleared: [key], updatedAt }.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"400":{"description":"VALIDATION_ERROR — empty/invalid `values` object, or per-key failures (unknown key, key is a trait not an attribute, wrong-scope key, bad value for the type/enum); details: [{ key, error }]. Nothing is applied on any failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"ENTITY_NOT_FOUND.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/contacts/{id}/attributes":{"put":{"tags":["ackDB"],"summary":"Set/clear attribute values on a contact","description":"`:id` = contact id or email. Accepts contact-scope keys only. Same all-or-nothing validation, null-clears-DELETEs-the-row, and no-op-writes-emit-nothing semantics as the company variant; the `trait.updated` event lands on the contact's company timeline.","operationId":"putContactAttributes","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Contact reference — a contact id (ULID) or an email (looked up, lowercased). Email is unique per entity, not globally — a match across multiple entities returns 409 AMBIGUOUS_IDENTITY instead of guessing."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbAttributePutValuesRequest"}}}},"responses":{"200":{"description":"{ entityId, contactId, applied: {key: value}, cleared: [key], updatedAt }.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"400":{"description":"VALIDATION_ERROR — empty/invalid `values` object, or per-key failures; details: [{ key, error }]. Nothing is applied on any failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"CONTACT_NOT_FOUND.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"409":{"description":"AMBIGUOUS_IDENTITY — the email matches more than one contact across entities; details: [{ contactId, entityId }] for each candidate. Retry with an explicit contactId.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/persons/{id}/attributes":{"put":{"tags":["ackDB"],"summary":"Set/clear attribute values on a person","description":"`:id` = person id, email, or LinkedIn URL (durable person keys are globally unique, so a key hit is unambiguous — there is no AMBIGUOUS_IDENTITY case here, unlike contacts). Accepts person-scope keys only. Same all-or-nothing validation, null-clears-DELETEs-the-row, and no-op-writes-emit-nothing semantics as the other two variants.\n\nEvery real change emits `person.attribute.changed` on the person timeline with `entityId` and `contactId` null, including for humans with no affiliations. Existing employers do not own the change. Historical company-associated `trait.updated` rows remain readable.","operationId":"putPersonAttributes","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Person reference — a person id (ULID), an email, or a LinkedIn URL. Durable person keys are GLOBALLY unique per value (#329), so a key match resolves to exactly one human; merged ids resolve to the surviving person."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbAttributePutValuesRequest"}}}},"responses":{"200":{"description":"{ entityId (always null), personId, applied: {key: value}, cleared: [key], updatedAt }.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"400":{"description":"VALIDATION_ERROR — empty/invalid `values` object, or per-key failures; details: [{ key, error }]. Nothing is applied on any failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"PERSON_NOT_FOUND.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"422":{"description":"SCHEMA_VALIDATION_ERROR — `:id` looked like an email or LinkedIn URL but is not a usable person key (blank/garbage value, or a role inbox such as support@).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/persons":{"get":{"tags":["ackDB"],"summary":"List persons","description":"Paginated person list. Each row carries `affiliationCount` (contact edges), `keyCount` (durable keys) and `isInternal` (any edge with is_external=false), so a list view needs no follow-up calls. Tombstoned (merged) persons are hidden unless `status=merged`.","operationId":"listPersons","parameters":[{"name":"q","in":"query","schema":{"type":"string"},"description":"Case-insensitive substring on the name."},{"name":"status","in":"query","schema":{"type":"string","enum":["active","merged"],"default":"active"},"description":"Identity lifecycle — NOT employment state (that lives on each contact edge)."},{"name":"keyType","in":"query","schema":{"type":"string"},"description":"Only persons holding a durable key of this type, e.g. `linkedin_url`.","example":"linkedin_url"},{"name":"internal","in":"query","schema":{"type":"boolean"},"description":"true = internal staff (some edge has is_external=false); false = external humans only."},{"name":"affiliated","in":"query","schema":{"type":"boolean"},"description":"true = has ≥1 contact edge; false = edge-less (keyless singletons, or a person whose only contact was hard-deleted). Omit for both."},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}}],"responses":{"200":{"description":"`{ data: PersonListItem[], total, limit, offset }`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}}},"security":[{"apiKey":[]}]},"post":{"tags":["ackDB"],"summary":"Create a person by hand","description":"The split-runbook primitive: when two humans were fused onto one person (a shared inbox, a bad key), delete the offending key, create the second human here, then re-point their edge with `PATCH /contacts/{contactId} { personId }`.\n\nKey types must be person-scope (`linkedin_url`, `email_address`, or a universal type registered with `scope: person`). Values are normalized (lowercased email / canonical LinkedIn) and validated: blank or null-sentinel values and role inboxes (`info@`, `sales@`, …) are **422** — a mailbox is not a human. A key already owned by another person is **409** naming the owner: keys are first-writer-wins and are never re-pointed.\n\n**Email is a fallback anchor (#575).** If the submitted keys include BOTH a `linkedin_url` and an `email_address`, the email key is DROPPED at mint (the person is anchored by linkedin; the email would be a recyclable global pointer). The response then carries a `suppressed: { keyType, values, stored: false, reason }` block — this door has no contact edge, so a dropped email is not stored anywhere. An email-ONLY create still keeps the email as its sole anchor.\n\nNo creation event is emitted (a person mint is bookkeeping, like an ingest-auto-created company); an audit log is written instead.","operationId":"createPerson","requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbPersonCreateRequest"}}}},"responses":{"201":{"description":"`{ data: { person, keys } }` — the new person and its stored (normalized) keys.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"400":{"description":"Malformed body (name not a string, keys not an array, …).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"409":{"description":"A key already belongs to another person — the response names the owner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"422":{"description":"Not a person-scope key type, or a blank/garbage/role-inbox key value.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/persons/search":{"get":{"tags":["ackDB"],"summary":"Find the single closest person (per-field)","description":"The person analog of `/contacts/search` and `/entities/search`. Send whichever field you have — params are ANDed and at least one is required: `q` (name + every key value), `name`, `email`, `linkedin`, `identity` (any key value, optionally pinned with `identityType`). `email`/`linkedin` are normalized before matching, so a raw pasted URL or mixed-case address still lands an exact-tier hit.\n\nRanked case-insensitive substring (exact > prefix > contains); returns ONE active person, hydrated exactly like `GET /persons/{personId}` (person + keys + affiliations + recent events), plus a `matchedOn` block. `matchedOn.contacts` names WHERE the matched value lives: every stint whose edge carries it verbatim (email / contact identity links; former stints included, disabled excluded), each with `entity: { entityId, name, domain }` — so a hit through an old employer's address is attributable even though `data` reports the current one. A `name` hit returns `[]` (a name sits on every stint alike). No match → 404.","operationId":"searchPersons","parameters":[{"name":"q","in":"query","schema":{"type":"string"},"description":"Match across the name and all key values."},{"name":"name","in":"query","schema":{"type":"string"},"example":"Elly Chen"},{"name":"email","in":"query","schema":{"type":"string"},"example":"elly@acme.com"},{"name":"linkedin","in":"query","schema":{"type":"string"},"example":"https://www.linkedin.com/in/ellychen/"},{"name":"identity","in":"query","schema":{"type":"string"},"description":"Match any durable key value (product ids included)."},{"name":"identityType","in":"query","schema":{"type":"string"},"description":"Pin `identity` to one key type, e.g. `perplexity_user_id`."},{"name":"contactLimit","in":"query","schema":{"type":"integer","default":25,"maximum":200}},{"name":"eventLimit","in":"query","schema":{"type":"integer","default":10,"maximum":100}}],"responses":{"200":{"description":"`{ data: PersonProfile, matchedOn: { field, value, tier, contacts: [{ contactId, status, title, startedAt, endedAt, entity: { entityId, name, domain } }] } }`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"404":{"description":"No person matches the given fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"422":{"description":"No field param provided (need ≥1 of q/name/email/linkedin/identity).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/persons/merge-candidates":{"get":{"tags":["ackDB"],"summary":"The first-writer-wins conflict ledger","description":"Every durable-key write that hit a value owned by ANOTHER human lands here instead of merging (ackDB never auto-merges people). Rows carry the contested key, the evidence payload that asserted it, and both persons (hydrated with names). A repeat of the same conflict bumps `updatedAt` rather than adding a row, and a DISMISSED candidate stays dismissed on re-conflict.\n\nConsume the queue with `POST /persons/{personId}/merge` (one pair) or `POST /persons/merge-duplicates` (batch).","operationId":"listMergeCandidates","parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["open","merged","dismissed"],"default":"open"}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}}],"responses":{"200":{"description":"`{ data: MergeCandidate[] (+ personAName/personBName), total, limit, offset }`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"400":{"description":"Unknown status value.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/persons/integrity":{"get":{"tags":["ackDB"],"summary":"Person/contact integrity sweep","description":"Read-only, on-demand health report over the identity graph (#472). Four wound classes, each with an exact count and a sample capped at 25 rows:\n\n- `unregisteredEdgeEmails` — contacts whose email is not a person key AND whose person has NO `linkedin_url` anchor. Should be 0: such a person is invisible to email-keyed ingest, and the next push mints a duplicate human. Heal with `npm run backfill-email-keys`. Note (#575): linkedin-anchored contacts whose email is intentionally column-only are NOT counted — email is a fallback anchor, so their missing email key is by design, and the backfill deliberately skips them.\n- `keylessPersons` — active persons with zero durable keys; reachable only by legacy entity-scoped adoption, never by global resolution.\n- `sameNameSameCompany` — the same normalized name held by DIFFERENT persons at one company: the visible tell of a split human. A review list, not a bug list — two real people can share a name.\n- `domainMismatchEdges` — active contacts whose email domain (non-freemail) matches none of their company's `domain` links; candidates for `POST /contacts/{contactId}/move`.","operationId":"getPersonIntegrityReport","responses":{"200":{"description":"`{ data: { unregisteredEdgeEmails, keylessPersons, sameNameSameCompany, domainMismatchEdges } }`, each `{ count, sample[] }`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/persons/{personId}":{"get":{"tags":["ackDB"],"summary":"Person profile","description":"The person + durable keys + affiliations-as-stints + a recent slice of their cross-company timeline. Stints are CAPPED (`contactLimit`, default 25, max 200) — internal staff can hold hundreds of edges — while `totalAffiliations` always reports the true count; page through them at `GET /persons/{personId}/contacts`. A merged (tombstoned) id transparently serves its survivor.","operationId":"getPerson","parameters":[{"name":"personId","in":"path","required":true,"schema":{"type":"string"},"description":"Person id (ULID = persons.person_id). A merged (tombstoned) id resolves to its survivor."},{"name":"contactLimit","in":"query","schema":{"type":"integer","default":25,"maximum":200}},{"name":"eventLimit","in":"query","schema":{"type":"integer","default":20,"maximum":100}}],"responses":{"200":{"description":"`{ data: PersonProfile }`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"404":{"description":"Person not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]},"patch":{"tags":["ackDB"],"summary":"Update person-grain facts (avatarUrl)","description":"The person-level setter for facts about the HUMAN. Today that is `avatarUrl` — the photo URL projected onto every contact read (#372). `null` clears it; a string replaces it (latest non-null wins, like `name`). http(s) URLs only — ackDB stores the link, never the bytes. A merged (tombstoned) id resolves to its survivor first. No event is emitted — presentation metadata, the entity-logo precedent.","operationId":"updatePerson","parameters":[{"name":"personId","in":"path","required":true,"schema":{"type":"string"},"description":"Person id (ULID = persons.person_id). A merged (tombstoned) id resolves to its survivor."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbPersonUpdateRequest"}}}},"responses":{"200":{"description":"`{ data: Person }`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"400":{"description":"No updatable field provided, or an invalid avatarUrl (non-http(s), too long).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"Person not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]},"delete":{"tags":["ackDB"],"summary":"Delete a human (company timelines keep their evidence)","operationId":"deletePersonRecord","description":"Hard-deletes the person, archived and restorable, with their durable keys frozen as a tombstone.\n\n**Events are DETACHED, never deleted.** A Slack message in a customer's channel is that COMPANY's history — deleting the human must not rewrite it. Every `person_id`/`contact_id` pointer is nulled and archived, so a restore re-attaches them. Embedding chunks and conversations are untouched: the text did not change, only its attribution.\n\n**Goes:** the person row, its durable keys, person attributes, merge candidates naming it, its merge tombstones, every affiliation edge (with links, traits and relationship ownership), and its segment memberships (each preceded by an `exit`).","parameters":[{"name":"personId","in":"path","required":true,"schema":{"type":"string"},"description":"Person id (ULID)."},{"name":"dryRun","in":"query","required":false,"schema":{"type":"boolean"},"description":"Preview only. Runs the IDENTICAL resolution, cascade and reporting, then rolls back — so the preview is produced by the code that does the work, not by a second implementation that can drift."}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","maxLength":500,"description":"Why — stored on the archive header."},"actor":{"type":"string","description":"Who — console user email; null for service calls."},"dryRun":{"type":"boolean","description":"Same as ?dryRun=true, for clients that cannot send query params."}}},"examples":{"withReason":{"summary":"With an audit trail","value":{"reason":"test account from the June import","actor":"vansh@floqer.com"}}}}}},"responses":{"200":{"description":"Deleted (or previewed). Returns what was deleted, what was detached, what was deliberately retained, and the tombstone that now guards the record's identifiers.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbRecordDeletionResponse"}}}},"404":{"description":"No such record."},"409":{"description":"`MERGED_ENTITY` / `MERGED_PERSON` — the reference names a merge tombstone (delete the survivor; its tombstones go with it). `PENDING_INGEST_CONFLICT` — a queued ingest would resurrect the data or dead-letter on the missing record; retry once the queue drains."}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/persons/{personId}/events":{"get":{"tags":["ackDB"],"summary":"The person timeline (cross-company)","description":"Every event stamped with this human, newest first, INTERLEAVED across every company they have ever been affiliated with — the query that answers \"have we talked to Elly?\" once instead of N times. Backed by the partial `(person_id, timestamp DESC)` index. Source events appear as `<source>_<event_type>` (`emails_message.sent`); ackDB's own lifecycle events are BARE (`contact.created`, `contact.ended`, historical `person.merged`, new `person.identity.merged`) with `source: system`. Independent personal events have null company/contact IDs and remain on the human even after an affiliation is added. Includes independent personal events. Filters apply before pagination; personal ownership with entityId is invalid.","operationId":"listPersonEvents","parameters":[{"name":"personId","in":"path","required":true,"schema":{"type":"string"},"description":"Person id (ULID = persons.person_id). A merged (tombstoned) id resolves to its survivor."},{"name":"limit","in":"query","schema":{"type":"integer","default":20,"maximum":100}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}},{"name":"source","in":"query","schema":{"type":"string"},"example":"slack"},{"name":"sources","in":"query","schema":{"type":"string"},"example":"slack,stripe","description":"CSV include list — only these sources. Mutually exclusive with `source`/`excludeSources` (400)."},{"name":"excludeSources","in":"query","schema":{"type":"string"},"example":"system,custom,member","description":"CSV exclude list — drop these sources (e.g. ackDB bookkeeping events). Mutually exclusive with `source`/`sources` (400)."},{"name":"eventType","in":"query","schema":{"type":"string"},"example":"contact.created"},{"name":"since","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"until","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"ownership","in":"query","required":false,"schema":{"type":"string","enum":["person","company"]},"description":"person selects sole-person events; company selects company-associated events; omit for both. Filters before pagination/counts. Personal ownership conflicts with entityId/company graph."},{"name":"entityId","in":"query","schema":{"type":"string"},"description":"Company filter before pagination/counts."}],"responses":{"200":{"description":"`{ data: EntityEvent[], total, limit, offset, personId }`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbEntityEvent"}},"total":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"},"personId":{"type":"string"}},"required":["data","total","limit","offset","personId"]}}}},"400":{"description":"More than one of `source`, `sources`, `excludeSources` was sent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"Person not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/persons/{personId}/contacts":{"get":{"tags":["ackDB"],"summary":"The stint list — every affiliation edge","description":"All contact edges for this human, active first then most-recently-seen, INCLUDING `former` ones: a job change leaves the old edge behind (`endedAt` stamped) and that history is the point. Each row is the contact plus its company name. `total` is the true edge count.","operationId":"listPersonContacts","parameters":[{"name":"personId","in":"path","required":true,"schema":{"type":"string"},"description":"Person id (ULID = persons.person_id). A merged (tombstoned) id resolves to its survivor."},{"name":"limit","in":"query","schema":{"type":"integer","default":100,"maximum":200}}],"responses":{"200":{"description":"`{ data: PersonAffiliation[], total, limit, personId }`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"404":{"description":"Person not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/persons/{personId}/links":{"get":{"tags":["ackDB"],"summary":"List a person's durable keys","description":"The keys that resolve to this human, with provenance (`evidencePayloadId` = the ingest that asserted the key). Pass `?type=` to filter. Keys accrete over a career — several emails are normal; a second `linkedin_url` is not.","operationId":"listPersonLinks","parameters":[{"name":"personId","in":"path","required":true,"schema":{"type":"string"},"description":"Person id (ULID = persons.person_id). A merged (tombstoned) id resolves to its survivor."},{"name":"type","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ data: PersonIdentityLink[] }`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"404":{"description":"Person not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]},"post":{"tags":["ackDB"],"summary":"Attach a durable key","description":"Scope-validated (person-scope types only), normalized (lowercased email / canonical LinkedIn), and **first-writer-wins**: a value owned by another human is **409** — the response names the owning person AND the `merge_candidates` row it just recorded, and nothing is written. Adding a SECOND `linkedin_url` to one person is also 409 + candidate (a slug change and two different humans look identical from here — a person decides). Re-adding a key the person already holds is an idempotent 200.\n\n**Email is a fallback anchor (#575).** An `email_address` is added only when the person has NO `linkedin_url` anchor. A clean email on a linkedin-anchored person is DECLINED with **200** `{ data: null, suppressed: true, stored: false, reason }` — this door has no contact edge, so the value is not stored anywhere (set it on a contact to keep it as `contacts.email` context). A conflict still wins first: an email owned by another human is 409 + candidate even on a linkedin-anchored person.","operationId":"addPersonLink","parameters":[{"name":"personId","in":"path","required":true,"schema":{"type":"string"},"description":"Person id (ULID = persons.person_id). A merged (tombstoned) id resolves to its survivor."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbPersonLinkCreateRequest"}}}},"responses":{"200":{"description":"The person already held this key — nothing changed.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"201":{"description":"The stored key (`PersonIdentityLink`).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"400":{"description":"Missing externalIdType/externalIdValue.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"Person not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"409":{"description":"Owned by another person, or a second linkedin_url — merge candidate recorded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"422":{"description":"Not a person-scope key type, or a blank/garbage/role-inbox value.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/persons/{personId}/links/{type}/{value}":{"delete":{"tags":["ackDB"],"summary":"Remove a durable key","description":"Drops the key (every source stamp of it) — the split-runbook primitive: un-key the wrong human, then `POST /persons` + `PATCH /contacts/{contactId} { personId }` to re-point the edge. The value is normalized before lookup, so the canonical stored form is matched; URL-encode it in the path.","operationId":"deletePersonLink","parameters":[{"name":"personId","in":"path","required":true,"schema":{"type":"string"},"description":"Person id (ULID = persons.person_id). A merged (tombstoned) id resolves to its survivor."},{"name":"type","in":"path","required":true,"schema":{"type":"string"},"example":"email_address"},{"name":"value","in":"path","required":true,"schema":{"type":"string"},"description":"URL-encoded key value.","example":"elly%40acme.com"}],"responses":{"204":{"description":"Key removed."},"404":{"description":"Person not found, or the person does not hold this key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"422":{"description":"Blank/garbage key value.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/persons/{personId}/merge":{"post":{"tags":["ackDB"],"summary":"Merge two persons","description":"Fuses two humans. The path person wins by default; `winnerOverride` (which must be one of the two ids) flips that. One transaction: keys move (exact dups dropped), the loser's edges re-point to the winner — a (person, company) collision folds the two edges via the contact-merge sub-cascade (events re-pointed, links/traits/ownership merged) — `entity_events.person_id` is restamped, the loser is tombstoned (`merged_into`), open candidates between the pair are closed, and `person.identity.merged` lands on the winner's person timeline with no company/contact, including when the winner has no affiliations. Historical company-associated `person.merged` rows remain readable.\n\nBoth ids are resolved through merge chains first, so a duplicate or concurrent merge is a **no-op** (`merged: false`), never an error. This endpoint and the batch one are the ONLY ways persons merge in ackDB — ingest never does.","operationId":"mergePersons","parameters":[{"name":"personId","in":"path","required":true,"schema":{"type":"string"},"description":"Person id (ULID = persons.person_id). A merged (tombstoned) id resolves to its survivor."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbPersonMergeRequest"}}}},"responses":{"200":{"description":"`{ data: PersonMergeResult }`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"400":{"description":"Missing loserId, or winnerOverride is not one of the two persons.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"Person not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/{entityId}/contacts":{"get":{"tags":["ackDB"],"summary":"List a company's people","description":"One row per affiliation edge, each carrying the `personId` of the human behind it — the company-side mirror of `GET /persons/{personId}/contacts`.\n\n**`status`** picks which affiliations count (#430):\n\n- `active` (default) — people who work there now. This is what \"who works at Acme\" means, and it is what `/profile`, `/full` and the AI agent read.\n- `former` — ended affiliations only, each stamped with `endedAt`.\n- `any` — both, current people first (then newest `lastSeenAt`). Use this to show departures alongside current staff — the champion who left in March is often the whole story.\n\n`any` never includes `disabled` edges: that status is the soft delete behind `DELETE /entities/{entityId}/contacts/{contactId}`, so a removed contact stays removed.","operationId":"listEntityContacts","parameters":[{"name":"entityId","in":"path","required":true,"schema":{"type":"string"},"description":"Company id."},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["active","former","any"],"default":"active"},"description":"Which affiliations to return. Anything else is a 400."},{"name":"is_external","in":"query","required":false,"schema":{"type":"boolean"},"description":"true = customer-side contacts only; false = internal staff only. Omit for both."}],"responses":{"200":{"description":"`{ data: Contact[] }` — each with `personId`, `status`, and `endedAt` when former.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"400":{"description":"Unrecognised `status` value.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"Entity not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/{entityId}/contacts/{contactId}":{"put":{"tags":["ackDB"],"summary":"Update contact properties (with transition detection)","description":"Only provided fields are written. Transitions become EVENTS (events are truth, state is cache):\n\n- `title` changing from one non-null value to a different one ⇒ `contact.role_changed` { from_title, to_title }\n- `status: \"former\"` ⇒ the affiliation ENDED: `endedAt` is stamped (body value or now) and `contact.ended` { reason: \"manual\" } is emitted — the manual sibling of a source-asserted job change.\n\nReactivating a `former` edge is **422**: an ended stint is history and is never resurrected. A returning employee gets a NEW stint row from the normal resolution path.\n\n`linkedinUrl` is NOT a contact column — it is a durable key of the contact's PERSON. Setting it here writes `person_identity_links` (normalized to `linkedin.com/in/<slug>`), so the value is visible to identity resolution instead of being an inert string. It is therefore deployment-wide and first-writer-wins: a LinkedIn already owned by a DIFFERENT human is **409** + a `merge_candidates` row, never a silent re-point. Send `null` to unlink it. Every affiliation of the same human shows the same LinkedIn — that is the point.\n\n`email` is a PAIRED write (#472): the edge's context column AND the person's `email_address` durable key, applied together so the human stays findable by email. First-writer-wins like linkedin: a value owned by a DIFFERENT person is **409** + merge candidate, and a value already used by another contact at this company is **409** (one address, one edge). Role/shared inboxes (info@, support@, …) write the column only — a mailbox is not a human. `null` clears the column; the person's keys are never auto-removed (that is `DELETE /persons/{id}/links/...`). A CHANGED email that contradicts the company's `domain` links emits `contact.domain_mismatch` and returns a `warnings[]` entry.","operationId":"updateContact","parameters":[{"name":"entityId","in":"path","required":true,"schema":{"type":"string"}},{"name":"contactId","in":"path","required":true,"schema":{"type":"string"},"description":"Contact id (ULID = contacts.contact_id) — the affiliation edge."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbContactUpdateRequest"}}}},"responses":{"200":{"description":"`{ data: Contact, emittedEvents: string[], warnings?: string[] }`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"400":{"description":"Invalid status or endedAt.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"Contact not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"409":{"description":"`linkedinUrl`/`email` is a durable key of another person (merge candidate recorded), or the email is already another contact's work email at this company.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"422":{"description":"Attempt to reactivate a former (ended) affiliation, or an invalid `linkedinUrl`/`email` value.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]},"delete":{"tags":["ackDB"],"summary":"Soft-delete (disable) a contact","description":"Sets status = disabled. The row, its links, traits and events all stay. For a genuine mis-attachment use `POST /contacts/{contactId}/move` (fix it) or `DELETE /contacts/{contactId}` (hard delete).","operationId":"disableContact","parameters":[{"name":"entityId","in":"path","required":true,"schema":{"type":"string"}},{"name":"contactId","in":"path","required":true,"schema":{"type":"string"},"description":"Contact id (ULID = contacts.contact_id) — the affiliation edge."}],"responses":{"204":{"description":"Contact disabled."},"404":{"description":"Contact not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/contacts/{contactId}/move":{"post":{"tags":["ackDB"],"summary":"Re-parent a mis-attached contact to the right company","description":"The fix for a contact that was attached to the wrong company. A move is a **CORRECTION, not a job change**, so it RESTAMPS the edge's events onto the corrected company (`entity_events.entity_id`): that history was always about the target company — only the pointer was wrong. `contact.moved` { from_entity_id, to_entity_id } is emitted on the NEW company. The person is untouched.\n\n**409** when the move would collide at the target: the person already has an active edge there (that is the same affiliation — merge the contacts instead of duplicating them), or an active contact there already holds this email.\n\nA genuine job change is NOT a move — it is a new contact at the new company (and the old edge ends). Never restamp a transition.","operationId":"moveContact","parameters":[{"name":"contactId","in":"path","required":true,"schema":{"type":"string"},"description":"Contact id (ULID = contacts.contact_id) — the affiliation edge."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbContactMoveRequest"}}}},"responses":{"200":{"description":"`{ data: Contact, moved: { fromEntityId, toEntityId, eventsRestamped } }`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"400":{"description":"Missing entityId, or the contact already belongs to it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"Contact or target entity not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"409":{"description":"The person (or the email) already has an active contact at the target company.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/contacts/{contactId}":{"patch":{"tags":["ackDB"],"summary":"Re-point a contact to another person (the split primitive)","description":"Moves the affiliation EDGE onto a different human. Used to unpick a wrong fusion (a shared inbox, a recycled email): `DELETE /persons/{id}/links/{type}/{value}` → `POST /persons` → this call. Also a CORRECTION ⇒ the edge's events are restamped with the new `person_id`, which is what preserves the invariant `event.person_id === contact.person_id`.\n\nThe target person must be active (a tombstone resolves to its survivor) and must not already hold an active edge at this company (that would be the same affiliation twice — merge instead).","operationId":"repointContact","parameters":[{"name":"contactId","in":"path","required":true,"schema":{"type":"string"},"description":"Contact id (ULID = contacts.contact_id) — the affiliation edge."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbContactRepointRequest"}}}},"responses":{"200":{"description":"`{ data: Contact, repointed: { fromPersonId, toPersonId, eventsRestamped } }` — or `repointed: false` when the edge already belonged to that person.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"400":{"description":"Missing personId.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"Contact or person not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"409":{"description":"Target person is merged, or already has an active contact at this company.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]},"delete":{"tags":["ackDB"],"summary":"Hard-delete a contact (full cascade)","description":"Irreversible. Deletes the edge and everything that hangs off it, in ONE transaction, in FK order:\n\n1. `entity_events.contact_id` → NULL, **`person_id` kept** — the events happened; the human keeps them on their timeline and only loses the (wrong) edge pointer\n2. `contact_identity_links` → deleted\n3. `contact_traits` → deleted\n4. `member_assignments` (relationship ownership) → deleted\n5. the contact row → deleted\n\nThe PERSON survives (they may hold other affiliations, keys and events). Counts are reported honestly. For a reversible removal use the soft delete at `DELETE /entities/{entityId}/contacts/{contactId}`.","operationId":"deleteContact","parameters":[{"name":"contactId","in":"path","required":true,"schema":{"type":"string"},"description":"Contact id (ULID = contacts.contact_id) — the affiliation edge."}],"responses":{"200":{"description":"`{ data: { contactId, entityId, personId, eventsDetached, identityLinksDeleted, traitsDeleted, memberAssignmentsDeleted } }`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"404":{"description":"Contact not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]},"get":{"tags":["ackDB"],"summary":"Read one contact by exact ID","description":"Returns one affiliation by its exact ackDB contact ID, hydrated with identity links, traits/attributes, compact company provenance, and the person's other affiliations. This is status-agnostic: retained former and disabled contacts remain directly addressable. The response includes an exact-tier `matchedOn` block for the contact ID.","operationId":"getContact","parameters":[{"name":"contactId","in":"path","required":true,"schema":{"type":"string"},"description":"Contact id (ULID = contacts.contact_id) — the affiliation edge."}],"responses":{"200":{"description":"`{ data: RawContactDetail, matchedOn: { field: 'contactId', value, tier: 'exact' } }`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"]}}}},"404":{"description":"CONTACT_NOT_FOUND.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/scores":{"get":{"tags":["ackDB"],"summary":"List score definitions","description":"Every definition (both grains), newest first. `?grain=` narrows to one grain.","operationId":"listScores","parameters":[{"name":"grain","in":"query","required":false,"schema":{"type":"string","enum":["company","person"]},"description":"Disambiguates a scoreKey that exists on both grains (400 without it in that case)."}],"responses":{"200":{"description":"The definitions.","content":{"application/json":{"schema":{"type":"object","properties":{"scores":{"type":"array","items":{"$ref":"#/components/schemas/AckdbScoreDefinition"}}},"required":["scores"]}}}}},"security":[{"apiKey":[]}]},"post":{"tags":["ackDB"],"summary":"Create a score definition","description":"Validates every field reference against the live schema (unknown refs → 400 with per-criterion errors in `error.details.errors`, each carrying `criterionIndex` for inline builder display). `isPrimary: true` transactionally takes over the grain's primary slot. Event refs are wire-BARE (`event:slack.message`) — the server resolves stored names. String match values on allowed-values traits are canonicalized case-insensitively.","operationId":"createScore","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbScoreDefinitionInput"}}}},"responses":{"201":{"description":"The created definition.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbScoreDefinition"}}}},"400":{"description":"Validation failed (per-criterion errors in error.details.errors).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"409":{"description":"A definition with this scoreKey already exists on this grain.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/scores/preview":{"post":{"tags":["ackDB"],"summary":"Preview a DRAFT definition (unsaved)","description":"Validates + compiles the draft body and returns the live population's score distribution — the builder histogram. Percentile drafts rank inline (exact), so nothing needs saving first. percentilePopulation: \"active\" drafts histogram the active rows only; population/excluded report the split (#432).","operationId":"previewScoreDraft","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbScoreDefinitionInput"}}}},"responses":{"200":{"description":"The score distribution.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbScorePreview"}}}},"400":{"description":"Validation failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/scores/capabilities":{"get":{"tags":["ackDB"],"summary":"What this server's score validator accepts","description":"The scoring contract: aggregate reducers (with the grains and scoring modes each allows), match operators, normalizations, percentile populations, convergence modes and every numeric limit. Builders should render from this rather than probing for support by saving throwaway definitions. Note eventValueMatching: match compares event metadata EXACTLY (case-sensitive), the same way segment filters do — seed value pickers from /fields/{key}/values instead of accepting free text (#438).","operationId":"getScoreCapabilities","responses":{"200":{"description":"The scoring capability contract.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbScoreCapabilities"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/scores/{scoreKey}":{"get":{"tags":["ackDB"],"summary":"Get a score definition","operationId":"getScore","parameters":[{"name":"scoreKey","in":"path","required":true,"schema":{"type":"string"},"description":"The score definition's slug (immutable after create)."},{"name":"grain","in":"query","required":false,"schema":{"type":"string","enum":["company","person"]},"description":"Disambiguates a scoreKey that exists on both grains (400 without it in that case)."}],"responses":{"200":{"description":"The definition.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbScoreDefinition"}}}},"400":{"description":"The key exists on both grains — pass ?grain=.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"No such definition.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]},"put":{"tags":["ackDB"],"summary":"Replace a score definition's config","description":"Full replace of the mutable fields (name, description, groups, criteria, normalization, labels, isPrimary, enabled). `scoreKey` and `grain` are immutable. Disabling a previously-enabled definition removes its columns and transactionally scrubs referencing saved segments (the response's `scrubbedSegments` reports how many). Any successful edit clears `degraded`.","operationId":"updateScore","parameters":[{"name":"scoreKey","in":"path","required":true,"schema":{"type":"string"},"description":"The score definition's slug (immutable after create)."},{"name":"grain","in":"query","required":false,"schema":{"type":"string","enum":["company","person"]},"description":"Disambiguates a scoreKey that exists on both grains (400 without it in that case)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbScoreDefinitionInput"}}}},"responses":{"200":{"description":"The updated definition (+ scrubbedSegments).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbScoreDefinition"}}}},"400":{"description":"Validation failed / immutable field changed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"No such definition.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]},"delete":{"tags":["ackDB"],"summary":"Delete a score definition","description":"Hard delete + transactional scrub of every saved segment referencing the score's columns (filters, sort, columns) — referencing views degrade gracefully instead of 400ing. Nothing is stored per record, so there is no other cleanup.","operationId":"deleteScore","parameters":[{"name":"scoreKey","in":"path","required":true,"schema":{"type":"string"},"description":"The score definition's slug (immutable after create)."},{"name":"grain","in":"query","required":false,"schema":{"type":"string","enum":["company","person"]},"description":"Disambiguates a scoreKey that exists on both grains (400 without it in that case)."}],"responses":{"200":{"description":"Deletion result.","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"},"scrubbedSegments":{"type":"integer","description":"Saved segments rewritten by the scrub."}},"required":["deleted","scrubbedSegments"]}}}},"404":{"description":"No such definition.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/scores/{scoreKey}/preview":{"get":{"tags":["ackDB"],"summary":"Preview a STORED definition","description":"The stored definition's score distribution over the live population.","operationId":"previewScore","parameters":[{"name":"scoreKey","in":"path","required":true,"schema":{"type":"string"},"description":"The score definition's slug (immutable after create)."},{"name":"grain","in":"query","required":false,"schema":{"type":"string","enum":["company","person"]},"description":"Disambiguates a scoreKey that exists on both grains (400 without it in that case)."}],"responses":{"200":{"description":"The score distribution.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbScorePreview"}}}},"404":{"description":"No such definition.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/{ref}/scores/{scoreKey}/explain":{"get":{"tags":["ackDB"],"summary":"Explain a company's score","description":"Per-criterion breakdown of a COMPANY-grain score for one company (entity id or domain): matched values / aggregate counts, points before and after group weighting, plus the normalized score and label — the same math the sc__ column computes.","operationId":"explainEntityScore","parameters":[{"name":"ref","in":"path","required":true,"schema":{"type":"string"},"description":"Entity id or domain."},{"name":"scoreKey","in":"path","required":true,"schema":{"type":"string"},"description":"The score definition's slug (immutable after create)."}],"responses":{"200":{"description":"The breakdown.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbScoreExplain"}}}},"404":{"description":"No such company or definition.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/contacts/{contactId}/scores/{scoreKey}/explain":{"get":{"tags":["ackDB"],"summary":"Explain a contact's person score","description":"PERSON-grain explain keyed by a contact id: the contact resolves to its PERSON, whose score is explained (person scores are computed on the human, across all their affiliations). A lead with no person 404s.","operationId":"explainContactScore","parameters":[{"name":"contactId","in":"path","required":true,"schema":{"type":"string"},"description":"Contact id (affiliation edge)."},{"name":"scoreKey","in":"path","required":true,"schema":{"type":"string"},"description":"The score definition's slug (immutable after create)."}],"responses":{"200":{"description":"The breakdown.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbScoreExplain"}}}},"404":{"description":"No such contact (or it has no person) or definition.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/persons/{personId}/scores/{scoreKey}/explain":{"get":{"tags":["ackDB"],"summary":"Explain a person's score","description":"Per-criterion breakdown of a PERSON-grain score for one human.","operationId":"explainPersonScore","parameters":[{"name":"personId","in":"path","required":true,"schema":{"type":"string"},"description":"Person id."},{"name":"scoreKey","in":"path","required":true,"schema":{"type":"string"},"description":"The score definition's slug (immutable after create)."}],"responses":{"200":{"description":"The breakdown.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbScoreExplain"}}}},"404":{"description":"No such person or definition.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/scores/snapshot":{"post":{"tags":["ackDB"],"summary":"Run the score time-series sweep","description":"Computes every ENABLED definition (both grains) over its live population — the same compile path as the sc__ columns — and appends CHANGE-ONLY `score.changed` events: first observation, label change, or |Δ| ≥ epsilon (default 0.5) vs the LAST EMITTED value (slow drift accumulates until it crosses the threshold). Naturally idempotent — a re-run with unchanged data emits nothing. The nightly cron runs this per tenant; this endpoint is the manual/API trigger.","operationId":"runScoreSnapshot","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"scoreKey":{"type":"string","description":"Narrow the sweep to one definition."},"epsilon":{"type":"number","description":"Emission threshold override (0–50; default 0.5)."}}}}}},"responses":{"200":{"description":"Sweep summary.","content":{"application/json":{"schema":{"type":"object","properties":{"definitions":{"type":"array","items":{"type":"object","properties":{"scoreKey":{"type":"string"},"grain":{"type":"string","enum":["company","person"]},"subjects":{"type":"integer","description":"Rows computed: the grain's active rows, narrowed to the definition's scope segment (#443)."},"emitted":{"type":"integer"},"exited":{"type":"integer","description":"Of `emitted`, EXIT emissions (#443): rows that were scored and have now left the definition's scope segment. Recorded as `score.changed` with `score: null` so a locf series stops instead of carrying a stale value. Scoped definitions only."},"skippedNoAnchor":{"type":"integer","description":"Person grain: humans with no affiliation edge to anchor the event."}},"required":["scoreKey","grain","subjects","emitted","exited","skippedNoAnchor"]}},"totalEmitted":{"type":"integer"},"epsilon":{"type":"number"}},"required":["definitions","totalEmitted","epsilon"]}}}},"400":{"description":"Invalid epsilon.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/{ref}/scores/{scoreKey}/history":{"get":{"tags":["ackDB"],"summary":"A company's score history","description":"The company's `score.changed` series (#411). History starts at the first sweep — the past is not reconstructable. A label change between consecutive points is a tier crossing.","operationId":"getEntityScoreHistory","parameters":[{"name":"ref","in":"path","required":true,"schema":{"type":"string"},"description":"Entity id or domain."},{"name":"scoreKey","in":"path","required":true,"schema":{"type":"string"},"description":"The score definition's slug (immutable after create)."},{"name":"from","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Window start (ISO). locf seeds from the latest emission at-or-before this."},{"name":"to","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Window end (ISO). Default: now."},{"name":"fill","in":"query","required":false,"schema":{"type":"string","enum":["changes","locf"]},"description":"changes (default): raw emissions. locf: daily series, last observation carried forward."}],"responses":{"200":{"description":"The score series.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbScoreHistory"}}}},"404":{"description":"No such subject or definition (deleted definitions keep their events but lose this surface).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/contacts/{contactId}/scores/{scoreKey}/history":{"get":{"tags":["ackDB"],"summary":"A contact's person-score history","description":"Resolves the contact to its PERSON and returns the person-grain series. Leads 404.","operationId":"getContactScoreHistory","parameters":[{"name":"contactId","in":"path","required":true,"schema":{"type":"string"},"description":"Contact id (affiliation edge)."},{"name":"scoreKey","in":"path","required":true,"schema":{"type":"string"},"description":"The score definition's slug (immutable after create)."},{"name":"from","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Window start (ISO). locf seeds from the latest emission at-or-before this."},{"name":"to","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Window end (ISO). Default: now."},{"name":"fill","in":"query","required":false,"schema":{"type":"string","enum":["changes","locf"]},"description":"changes (default): raw emissions. locf: daily series, last observation carried forward."}],"responses":{"200":{"description":"The score series.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbScoreHistory"}}}},"404":{"description":"No such subject or definition (deleted definitions keep their events but lose this surface).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/persons/{personId}/scores/{scoreKey}/history":{"get":{"tags":["ackDB"],"summary":"A person's score history","operationId":"getPersonScoreHistory","parameters":[{"name":"personId","in":"path","required":true,"schema":{"type":"string"},"description":"Person id."},{"name":"scoreKey","in":"path","required":true,"schema":{"type":"string"},"description":"The score definition's slug (immutable after create)."},{"name":"from","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Window start (ISO). locf seeds from the latest emission at-or-before this."},{"name":"to","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Window end (ISO). Default: now."},{"name":"fill","in":"query","required":false,"schema":{"type":"string","enum":["changes","locf"]},"description":"changes (default): raw emissions. locf: daily series, last observation carried forward."}],"responses":{"200":{"description":"The score series.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbScoreHistory"}}}},"404":{"description":"No such subject or definition (deleted definitions keep their events but lose this surface).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/deletions":{"get":{"tags":["ackDB"],"summary":"List the deletion archive (recently deleted)","operationId":"listDeletions","parameters":[{"name":"kind","in":"query","schema":{"type":"string","enum":["company","person","contact"]}},{"name":"status","in":"query","schema":{"type":"string","enum":["deleted","restored","purged"]}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"The last id of the previous page — ULIDs sort by creation time, so the id IS the cursor."}],"responses":{"200":{"description":"Archive headers, newest first.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbRecordDeletionHeader"}},"nextCursor":{"type":"string","nullable":true}}}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/deletions/{id}":{"get":{"tags":["ackDB"],"summary":"One archive entry + its per-table row counts","operationId":"getDeletion","description":"The header (what was deleted, by whom, why, its tombstone and the honest counts) plus how many rows of each table it holds. Never the rows themselves — an entry can hold hundreds of thousands.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The archive entry.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbRecordDeletionHeader"}}}}}},"404":{"description":"`DELETION_NOT_FOUND`."}},"security":[{"apiKey":[]}]},"delete":{"tags":["ackDB"],"summary":"Permanently purge the archived copy (the tombstone stays)","operationId":"purgeDeletion","description":"Destroys the archived rows and their file blobs for good. The header survives as the audit record, and **the tombstone stays ACTIVE** — destroying the copy is not permission to re-create the record. To allow that, lift the tombstone separately.\n\n`status` flips to `purged` only from `deleted`; a RESTORED entry keeps `status: restored` (its rows are live, and calling that state 'purged' would read as 'the record is gone').","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Purged. Reports the rows and blobs destroyed."},"404":{"description":"`DELETION_NOT_FOUND`."}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/deletions/{id}/restore":{"post":{"tags":["ackDB"],"summary":"Restore a deleted record","operationId":"restoreDeletion","description":"Puts every archived row back, in FK order, in one transaction, and clears the tombstone.\n\nOnly a `deleted` entry restores: a purged one is a 409 (there is nothing left to put back), a restored one is an idempotent no-op. **Fail-closed:** if any unique constraint would break — an identity link claimed after the tombstone was lifted, a person key, the `(entity_id, email)` active-unique, a re-used idempotency key — the restore is refused with every offender listed and NOTHING is written. No partial restores, ever.\n\nDerived content is NOT restored because it was never archived: restored payloads come back marked unprocessed, and `pendingRebuild` names the (entity, source) pairs the embed + conversations pipelines must rebuild. Segment memberships are likewise not restored — the next sweep recomputes and emits `enter` naturally, which is the honest signal.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Restored. Reports rows per table, re-attached pointers, and the pending rebuild."},"404":{"description":"`DELETION_NOT_FOUND`."},"409":{"description":"`ALREADY_PURGED` — the archived rows were destroyed. `RESTORE_CONFLICT` — something claimed one of the record's identifiers while it was deleted; every offender is listed and nothing was written."}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/dashboards":{"get":{"tags":["ackDB"],"summary":"List dashboards","description":"The dashboards the caller can see, newest-updated first. ACL is the segment ACL (#284): a service/static-key call (no JWT) sees everything as `owner`. `limit` is validated, never clamped (#480) - out of range is a 400.","operationId":"listDashboards","parameters":[{"name":"limit","in":"query","required":false,"description":"Page size (default 50, max 200).","schema":{"type":"integer","minimum":1,"maximum":200}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0}}],"responses":{"200":{"description":"A page of dashboard summaries.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbDashboardSummary"}},"pagination":{"type":"object","properties":{"total":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"}}}},"required":["data","pagination"]}}}},"400":{"description":"limit is non-numeric or outside [1, 200].","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]},"post":{"tags":["ackDB"],"summary":"Create a dashboard","description":"Create a dashboard. EVERY panel is validated: the pinned segment exists AND is visible to the caller, it is a BASE segment (derived segments are refused), the spec compiles against the grain's catalog, `sql` specs and artifact queries are execution-probed on the read-only role, and the artifact HTML is scanned for credentials. A 400 lists every failure at once under `error.details.errors[] = { panel, message }`. Ownership is stamped from the caller's JWT (null for a service call = org-visible), never from the body.","operationId":"createDashboard","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbDashboardCreateRequest"}}}},"responses":{"201":{"description":"The created dashboard, with the caller's effective access.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbDashboard"}},"required":["data"]}}}},"400":{"description":"Panel validation failed - see error.details.errors[] = { panel, message }.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/dashboards/preview":{"post":{"tags":["ackDB"],"summary":"Preview one draft panel","description":"Validate and RUN one panel without persisting anything. Unlike a saved-panel data read (which trusts the save-time gate) this validates HERE, including the caller's access to the pinned segment. The response is the same envelope `/data` returns; the cache is bypassed. Rate limited to 60 previews per minute per CALLER (service/static-key callers share one bucket): this door runs uncached SQL, including a `sql` panel's statement.","operationId":"previewDashboardPanel","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"panel":{"$ref":"#/components/schemas/AckdbDashboardPanel"}},"required":["panel"]}}}},"responses":{"200":{"description":"The panel envelope (data | degraded | error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbPanelEnvelope"}}}},"400":{"description":"The panel is malformed, names an unknown field, or pins an invisible/derived segment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"429":{"description":"Preview rate limit exceeded (60/min per caller). Carries `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/dashboards/{id}":{"get":{"tags":["ackDB"],"summary":"Get a dashboard","description":"The full record plus the caller's effective `access`.","operationId":"getDashboard","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Dashboard id (ULID).","example":"01J9Z8Q3K2X7M4N5P6R7S8T9V0"}],"responses":{"200":{"description":"The dashboard.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbDashboard"}},"required":["data"]}}}},"403":{"description":"The caller cannot see this dashboard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"DASHBOARD_NOT_FOUND - no such dashboard, or it is soft-deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]},"put":{"tags":["ackDB"],"summary":"Update a dashboard (patch)","description":"Edit access or better. An OMITTED field is left alone; an explicit `null` CLEARS `description` / `artifact` / `layout` (the SET list is built from the keys actually present). An empty patch does not bump `updatedAt`. Panels are re-validated, but only panels whose `(segmentId, type, spec)` actually MOVED are re-checked against the caller's segment access: an editor without access to a private segment may rename and rearrange, just not REPOINT.","operationId":"updateDashboard","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Dashboard id (ULID).","example":"01J9Z8Q3K2X7M4N5P6R7S8T9V0"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbDashboardUpdateRequest"}}}},"responses":{"200":{"description":"The updated dashboard.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbDashboard"}},"required":["data"]}}}},"400":{"description":"Panel validation failed - see error.details.errors[] = { panel, message }.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"403":{"description":"Edit access required.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"DASHBOARD_NOT_FOUND.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]},"delete":{"tags":["ackDB"],"summary":"Delete a dashboard","description":"Soft delete (tombstone). Owner only. A deleted dashboard leaves the list and every read.","operationId":"deleteDashboard","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Dashboard id (ULID).","example":"01J9Z8Q3K2X7M4N5P6R7S8T9V0"}],"responses":{"200":{"description":"Deletion outcome.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"deleted":{"type":"boolean"}}}}}}}},"403":{"description":"Only the dashboard owner can delete it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"DASHBOARD_NOT_FOUND.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/dashboards/{id}/shares":{"get":{"tags":["ackDB"],"summary":"List dashboard shares","description":"The grant set. Anyone who can see the dashboard can read it.","operationId":"listDashboardShares","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Dashboard id (ULID).","example":"01J9Z8Q3K2X7M4N5P6R7S8T9V0"}],"responses":{"200":{"description":"The grants.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbDashboardShare"}}},"required":["data"]}}}},"403":{"description":"The caller cannot see this dashboard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"DASHBOARD_NOT_FOUND.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]},"put":{"tags":["ackDB"],"summary":"Replace dashboard shares","description":"Atomic replace of the WHOLE grant set (the segment sharing contract verbatim). Owner only. Grants are roster-constrained to ACTIVE members; a duplicated memberId is deduped (last occurrence wins), not a 400.","operationId":"replaceDashboardShares","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Dashboard id (ULID).","example":"01J9Z8Q3K2X7M4N5P6R7S8T9V0"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"shares":{"type":"array","items":{"type":"object","properties":{"memberId":{"type":"string"},"role":{"type":"string","enum":["view","edit"]}},"required":["memberId","role"]}}},"required":["shares"]}}}},"responses":{"200":{"description":"The stored grant set.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbDashboardShare"}}},"required":["data"]}}}},"400":{"description":"Malformed body, or a memberId that is not an active member.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"403":{"description":"Only the dashboard owner can change sharing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"DASHBOARD_NOT_FOUND.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/dashboards/{id}/data":{"get":{"tags":["ackDB"],"summary":"Run one panel","description":"Run ONE saved panel and return its envelope. Authorization is on the DASHBOARD, not the pinned segment: sharing a dashboard shares what it shows. Always 200 for a data-shaped failure (the console fetches panels in parallel and renders each independently). Cached 30s, keyed on the panel spec + the segment's filterHash + its sync seq, so a filter edit or a membership change busts it immediately. Rate limited to 60 reads per dashboard per minute, applied AFTER authorization.","operationId":"getDashboardPanelData","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Dashboard id (ULID).","example":"01J9Z8Q3K2X7M4N5P6R7S8T9V0"},{"name":"panel","in":"query","required":true,"schema":{"type":"string"},"description":"The panel key.","example":"mrr_by_tier"}],"responses":{"200":{"description":"The panel envelope (data | degraded | error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbPanelEnvelope"}}}},"400":{"description":"The `panel` query parameter is missing, or names no panel on this dashboard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"403":{"description":"The caller cannot see this dashboard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"DASHBOARD_NOT_FOUND.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"429":{"description":"RATE_LIMITED - more than 60 data reads for this dashboard this minute. `Retry-After` is set.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/dashboards/{id}/freshness":{"get":{"tags":["ackDB"],"summary":"Segment sync freshness for a dashboard","description":"Per-segment sync position for every segment a non-degraded panel pins. Calling it also FIRES the membership diff for those segments (rate limited to one per segment per 60s), so polling it while a dashboard is open is what self-drives change detection. The diff is fire and forget: the response reflects the PREVIOUS diff, so at the console's ~30s cadence a one-poll lag is deliberate. This endpoint must stay fast, and freshness is an accelerator - live queries are the correctness mechanism. `seq` is GREATEST(max(seq), prunedThroughSeq), so retention pruning never looks like a rewind. A segment never diffed (or a deployment without the sync tables) reads `{ seq: 0, lastChangeAt: null }`.","operationId":"getDashboardFreshness","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Dashboard id (ULID).","example":"01J9Z8Q3K2X7M4N5P6R7S8T9V0"}],"responses":{"200":{"description":"Per-segment watermarks.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbDashboardFreshness"}}}},"403":{"description":"The caller cannot see this dashboard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"DASHBOARD_NOT_FOUND.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/dashboards/{id}/artifact/data":{"get":{"tags":["ackDB"],"summary":"Run the artifact's queries","description":"Run every query the dashboard's artifact declares (max 1000 rows each) and return them keyed by query key. A failing query is reported INLINE under its own key as `{ error: { code, message } }`, so one bad query does not blank the artifact. Shares the 60/min per-dashboard budget with `GET /observe/dashboards/{id}/data`: one request here runs EVERY declared query.","operationId":"getDashboardArtifactData","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Dashboard id (ULID).","example":"01J9Z8Q3K2X7M4N5P6R7S8T9V0"}],"responses":{"200":{"description":"Rows per artifact query key.","content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"object","additionalProperties":true}},"required":["results"]}}}},"403":{"description":"The caller cannot see this dashboard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"DASHBOARD_NOT_FOUND, or ARTIFACT_NOT_FOUND when the dashboard has no artifact.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"429":{"description":"Rate limit exceeded (60/min per dashboard, shared with /data). Carries `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/schemas/entity-relationship-types":{"get":{"summary":"List company relationship types","operationId":"listCompanyRelationshipTypes","tags":["ackDB"],"responses":{"200":{"description":"Relationship types","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbCompanyRelationshipType"}}}}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/schemas/entity-relationship-types/{typeKey}":{"get":{"summary":"Get a company relationship type","operationId":"getCompanyRelationshipType","tags":["ackDB"],"parameters":[{"name":"typeKey","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Company relationship type","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbCompanyRelationshipType"}},"required":["data"]}}}},"404":{"description":"Not found"}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entity-relationships":{"post":{"summary":"Create a company relationship episode","operationId":"createCompanyRelationship","description":"Idempotent for an exact retry whose active child/parent/type/effectiveFrom match. A different overlapping start returns 409. A domain endpoint that does not exist is created as graph-only. Emits entity.relationship.created only when created=true, timestamped when the mutation occurred.","tags":["ackDB"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"childCompany":{"$ref":"#/components/schemas/AckdbCompanyReferenceInput"},"parentCompany":{"$ref":"#/components/schemas/AckdbCompanyReferenceInput"},"relationshipType":{"type":"string"},"effectiveFrom":{"type":"string","format":"date-time"},"provenance":{"$ref":"#/components/schemas/AckdbCompanyRelationshipProvenance"},"actor":{"type":"string"}},"required":["childCompany","parentCompany","relationshipType","provenance"],"additionalProperties":false}}}},"responses":{"200":{"description":"Company relationship mutation result","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbCompanyRelationship"},"created":{"type":"boolean"}},"required":["data","created"]}}}},"201":{"description":"Company relationship mutation result","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbCompanyRelationship"},"created":{"type":"boolean"}},"required":["data","created"]}}}},"409":{"description":"Overlap"}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entity-relationships/{relationshipId}":{"get":{"summary":"Get a relationship episode by id","operationId":"getCompanyRelationship","tags":["ackDB"],"parameters":[{"name":"relationshipId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Company relationship","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbCompanyRelationship"}},"required":["data"]}}}},"404":{"description":"Not found"}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entity-relationships/{relationshipId}/end":{"post":{"summary":"End a relationship episode","operationId":"endCompanyRelationship","description":"Idempotent once ended. Emits entity.relationship.ended only when changed=true.","tags":["ackDB"],"parameters":[{"name":"relationshipId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"effectiveTo":{"type":"string","format":"date-time"},"reason":{"type":"string"},"actor":{"type":"string"}},"additionalProperties":false}}}},"responses":{"200":{"description":"Company relationship mutation result","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbCompanyRelationship"},"changed":{"type":"boolean"}},"required":["data","changed"]}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entity-relationships/{relationshipId}/retract":{"post":{"summary":"Retract a relationship episode","operationId":"retractCompanyRelationship","description":"Retraction removes the episode from every normal as-of read. Idempotent once retracted.","tags":["ackDB"],"parameters":[{"name":"relationshipId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string"},"actor":{"type":"string"}},"required":["reason"],"additionalProperties":false}}}},"responses":{"200":{"description":"Company relationship mutation result","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbCompanyRelationship"},"changed":{"type":"boolean"}},"required":["data","changed"]}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/{company}/relationships":{"get":{"summary":"Get direct company relationship counts and merge aliases","operationId":"getCompanyRelationshipSummary","tags":["ackDB"],"parameters":[{"name":"company","in":"path","required":true,"schema":{"type":"string"},"description":"Company entity id or domain. A merged id resolves to its canonical company."},{"name":"asOf","in":"query","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"Company relationship summary","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbCompanyRelationshipSummary"}},"required":["data"]}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/{company}/relationships/parents":{"get":{"summary":"List a company's direct parents","operationId":"listCompanyRelationshipParents","tags":["ackDB"],"parameters":[{"name":"company","in":"path","required":true,"schema":{"type":"string"},"description":"Company entity id or domain. A merged id resolves to its canonical company."},{"name":"asOf","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"relationshipType","in":"query","schema":{"type":"array","items":{"type":"string"}},"style":"form","explode":true},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Keyset-paged relationship data","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbCompanyRelationship"}},"nextCursor":{"type":"string","nullable":true}},"required":["data","nextCursor"]}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/{company}/relationships/children":{"get":{"summary":"List a company's direct children","operationId":"listCompanyRelationshipChildren","tags":["ackDB"],"parameters":[{"name":"company","in":"path","required":true,"schema":{"type":"string"},"description":"Company entity id or domain. A merged id resolves to its canonical company."},{"name":"asOf","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"relationshipType","in":"query","schema":{"type":"array","items":{"type":"string"}},"style":"form","explode":true},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Keyset-paged relationship data","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbCompanyRelationship"}},"nextCursor":{"type":"string","nullable":true}},"required":["data","nextCursor"]}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/{company}/relationships/merged-from":{"get":{"summary":"List company ids merged into this canonical company","operationId":"listCompanyRelationshipMergeAliases","tags":["ackDB"],"parameters":[{"name":"company","in":"path","required":true,"schema":{"type":"string"},"description":"Company entity id or domain. A merged id resolves to its canonical company."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Keyset-paged relationship data","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbCompanyIdentityRelationship"}},"nextCursor":{"type":"string","nullable":true}},"required":["data","nextCursor"]}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/{company}/graph":{"post":{"summary":"Traverse a company relationship graph","operationId":"getCompanyRelationshipGraph","description":"scope is parents, children, or connected. Omitted maxDepth means all reachable. Hard caps are 500 companies and 5000 relationships; cap errors never return partial data. includeRoot=false omits only the root company node and keeps incident relationships. One 8-second wall-clock deadline covers the complete read transaction.","tags":["ackDB"],"parameters":[{"name":"company","in":"path","required":true,"schema":{"type":"string"},"description":"Company entity id or domain. A merged id resolves to its canonical company."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"scope":{"type":"string","enum":["parents","children","connected"]},"relationshipTypes":{"type":"array","items":{"type":"string"}},"maxDepth":{"type":"integer","minimum":0,"maximum":499},"asOf":{"type":"string","format":"date-time"},"includeRoot":{"type":"boolean","default":true}},"required":["scope"],"additionalProperties":false}}}},"responses":{"200":{"description":"Bounded complete graph","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbCompanyGraphResult"}},"required":["data"]}}}},"422":{"description":"Graph cap exceeded"},"503":{"description":"Graph read exceeded its transaction deadline","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/{company}/graph/path":{"post":{"summary":"Find the shortest relationship path between two companies","operationId":"findCompanyRelationshipPath","tags":["ackDB"],"parameters":[{"name":"company","in":"path","required":true,"schema":{"type":"string"},"description":"Company entity id or domain. A merged id resolves to its canonical company."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"targetCompany":{"type":"string"},"relationshipTypes":{"type":"array","items":{"type":"string"}},"maxDepth":{"type":"integer","minimum":0,"maximum":499},"asOf":{"type":"string","format":"date-time"}},"required":["targetCompany"],"additionalProperties":false}}}},"responses":{"200":{"description":"Shortest path, or found=false","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbCompanyRelationshipPathResult"}},"required":["data"]}}}},"422":{"description":"Graph cap exceeded"},"503":{"description":"Graph read exceeded its transaction deadline","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/schemas/contact-relationship-types":{"get":{"summary":"List contact relationship types","operationId":"listContactRelationshipTypes","tags":["ackDB"],"responses":{"200":{"description":"Type definitions","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbContactRelationshipType"}}},"required":["data"]}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/schemas/contact-relationship-types/{typeKey}":{"get":{"summary":"Get a contact relationship type","operationId":"getContactRelationshipType","tags":["ackDB"],"parameters":[{"name":"typeKey","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Type definition","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbContactRelationshipType"}},"required":["data"]}}}},"404":{"description":"Not found"}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/contact-relationships":{"post":{"summary":"Create a reporting relationship episode","operationId":"createContactRelationship","description":"Creates reportContactId -> managerContactId. Exact open retries return 200 with created=false. Cross-company, self, overlapping, future, and temporally cyclic writes are rejected.","tags":["ackDB"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"reportContactId":{"type":"string"},"managerContactId":{"type":"string"},"relationshipType":{"type":"string","example":"reports_to"},"effectiveFrom":{"type":"string","format":"date-time"},"provenance":{"$ref":"#/components/schemas/AckdbContactRelationshipProvenance"},"actor":{"type":"string"}},"required":["reportContactId","managerContactId","relationshipType","provenance"],"additionalProperties":false}}}},"responses":{"200":{"description":"Contact reporting relationship mutation result","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbContactRelationship"},"created":{"type":"boolean"}},"required":["data","created"]}}}},"201":{"description":"Contact reporting relationship mutation result","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbContactRelationship"},"created":{"type":"boolean"}},"required":["data","created"]}}}},"409":{"description":"Conflict"}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/contact-relationships/{relationshipId}":{"get":{"summary":"Get one contact relationship episode","operationId":"getContactRelationship","tags":["ackDB"],"parameters":[{"name":"relationshipId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Contact reporting relationship episode","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbContactRelationship"}},"required":["data"]}}}},"404":{"description":"Not found"}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/contact-relationships/{relationshipId}/end":{"post":{"summary":"End a contact relationship episode","operationId":"endContactRelationship","tags":["ackDB"],"parameters":[{"name":"relationshipId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"effectiveTo":{"type":"string","format":"date-time"},"reason":{"type":"string"},"actor":{"type":"string"}},"additionalProperties":false}}}},"responses":{"200":{"description":"Contact reporting relationship mutation result","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbContactRelationship"},"changed":{"type":"boolean"}},"required":["data","changed"]}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/contact-relationships/{relationshipId}/retract":{"post":{"summary":"Retract an invalid contact relationship episode","operationId":"retractContactRelationship","tags":["ackDB"],"parameters":[{"name":"relationshipId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string"},"actor":{"type":"string"}},"required":["reason"],"additionalProperties":false}}}},"responses":{"200":{"description":"Contact reporting relationship mutation result","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbContactRelationship"},"changed":{"type":"boolean"}},"required":["data","changed"]}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/contacts/{contactId}/relationships":{"get":{"summary":"Get direct reporting relationship counts","operationId":"getContactRelationshipSummary","tags":["ackDB"],"parameters":[{"name":"contactId","in":"path","required":true,"schema":{"type":"string"},"description":"Contact affiliation ULID (not person id)."},{"name":"asOf","in":"query","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"Summary","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbContactRelationshipSummary"}},"required":["data"]}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/contacts/{contactId}/relationships/managers":{"get":{"summary":"List a contact's direct managers","operationId":"listContactManagers","tags":["ackDB"],"parameters":[{"name":"contactId","in":"path","required":true,"schema":{"type":"string"},"description":"Contact affiliation ULID (not person id)."},{"name":"asOf","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"relationshipType","in":"query","schema":{"type":"array","items":{"type":"string"}},"style":"form","explode":true},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200}},{"name":"cursor","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Keyset page of active as-of relationships","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbContactRelationship"}},"nextCursor":{"type":"string","nullable":true}},"required":["data","nextCursor"]}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/contacts/{contactId}/relationships/direct-reports":{"get":{"summary":"List a contact's direct reports","operationId":"listContactDirectReports","tags":["ackDB"],"parameters":[{"name":"contactId","in":"path","required":true,"schema":{"type":"string"},"description":"Contact affiliation ULID (not person id)."},{"name":"asOf","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"relationshipType","in":"query","schema":{"type":"array","items":{"type":"string"}},"style":"form","explode":true},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200}},{"name":"cursor","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Keyset page of active as-of relationships","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AckdbContactRelationship"}},"nextCursor":{"type":"string","nullable":true}},"required":["data","nextCursor"]}}}}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/contacts/{contactId}/graph":{"post":{"summary":"Traverse a contact reporting hierarchy","operationId":"queryContactGraph","description":"Bounded, complete as-of traversal. Hard caps are 5,000 contacts and 20,000 relationships; cap failures return no partial graph.","tags":["ackDB"],"parameters":[{"name":"contactId","in":"path","required":true,"schema":{"type":"string"},"description":"Contact affiliation ULID (not person id)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"scope":{"type":"string","enum":["managers","directReports","connected"]},"relationshipTypes":{"type":"array","items":{"type":"string"}},"maxDepth":{"type":"integer","minimum":0,"maximum":4999},"asOf":{"type":"string","format":"date-time"},"includeRoot":{"type":"boolean","default":true}},"required":["scope"],"additionalProperties":false}}}},"responses":{"200":{"description":"Contact reporting graph","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbContactGraphResult"}},"required":["data"]}}}},"422":{"description":"Graph cap exceeded"},"503":{"description":"Read deadline exceeded"}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/contacts/{contactId}/graph/path":{"post":{"summary":"Find the shortest reporting path between two contacts","operationId":"findContactPath","tags":["ackDB"],"parameters":[{"name":"contactId","in":"path","required":true,"schema":{"type":"string"},"description":"Contact affiliation ULID (not person id)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"targetContactId":{"type":"string"},"relationshipTypes":{"type":"array","items":{"type":"string"}},"maxDepth":{"type":"integer","minimum":0,"maximum":4999},"asOf":{"type":"string","format":"date-time"}},"required":["targetContactId"],"additionalProperties":false}}}},"responses":{"200":{"description":"Shortest path, or found=false","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbContactPathResult"}},"required":["data"]}}}},"409":{"description":"Contacts belong to different companies"}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/{entityId}/contact-graph":{"post":{"summary":"Get a company's complete current contact hierarchy","operationId":"getCompanyContactGraph","description":"Returns relationship-connected contact nodes only. Use the ordinary company contact list for unassigned contacts.","tags":["ackDB"],"parameters":[{"name":"entityId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"relationshipTypes":{"type":"array","items":{"type":"string"}},"asOf":{"type":"string","format":"date-time"}},"additionalProperties":false}}}},"responses":{"200":{"description":"Contact reporting graph","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AckdbContactGraphResult"}},"required":["data"]}}}},"422":{"description":"Graph cap exceeded"}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/entities/merge/preview":{"post":{"tags":["ackDB"],"summary":"Preview an ordered manual entity merge","description":"Runs the real company-relationship reconciliation for the supplied winner and loser in a rollback-only transaction. No data moves and no event persists. Use the same ordered pair with POST /entities/merge to apply.","operationId":"previewEntityMerge","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbMergeRequest"}}}},"responses":{"200":{"description":"Relationship impact preview","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"winnerId":{"type":"string"},"loserId":{"type":"string"},"relationshipImpact":{"$ref":"#/components/schemas/AckdbCompanyRelationshipMergeImpact"}},"required":["winnerId","loserId","relationshipImpact"]}},"required":["data"]}}}},"404":{"description":"Entity not found"},"409":{"description":"Entity is not active"}},"security":[{"apiKey":[]}]}},"/api/v1/ackdb/observe/segments/{id}/bootstrap-page":{"post":{"tags":["ackDB"],"summary":"Advance one bounded segment bootstrap page","description":"Internal Segment Sync expansion contract: evaluates at most one keyset page, returns those exact member ids, and inserts the same ids into an unfinished ackDB snapshot in one short transaction. A downstream ledger consumes this response directly, so initial sync no longer runs the segment filter once for the snapshot and again for the ledger. Replaying an already covered `after` page is idempotent; a cursor ahead of durable progress is rejected. Existing bootstrapped segments use the same endpoint as a live read for additional connections. Pass the first page's `filterHash` as `expectedFilterHash` on later pages; a mid-seed filter edit returns 409 FILTER_CHANGED before writing a mixed page.","operationId":"bootstrapSegmentPage","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"after","in":"query","required":false,"description":"Keyset cursor from the previous page. Omit for page one or a replay of page one.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Page size (default 1000, max 5000).","schema":{"type":"integer","minimum":1,"maximum":5000}},{"name":"expectedFilterHash","in":"query","required":false,"description":"Opaque filter version returned by page one; later pages fail closed if it changed.","schema":{"type":"string"}}],"responses":{"200":{"description":"The exact member page used to advance the snapshot.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbSegmentBootstrapPageResponse"}}}},"400":{"description":"Cross-grain request, unknown query parameter, or stale filter.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"404":{"description":"No segment with that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"409":{"description":"LOCK_BUSY, FILTER_CHANGED, or BOOTSTRAP_CURSOR_INVALID; no work was committed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}},"503":{"description":"SYNC_TABLES_UNAVAILABLE - the segment-sync migration has not run.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AckdbErrorResponse"}}}}},"security":[{"apiKey":[]}]}}},"servers":[{"url":"http://localhost:3009","description":"Local Development"},{"url":"https://api.floqer.com","description":"Production"}],"security":[{"apiKey":[]}],"tags":[{"name":"User","description":"Caller identity and org context — the bootstrap call for an agent. `GET /api/v1/user` returns who the API key belongs to (email, name, org role) and the organization's member roster. Call this first to confirm identity before building or running workflows."},{"name":"Organization","description":"Your organization: who is in it, and what it knows.\n\n**Members.** Invite a person — they get an email invitation and hold no access until they accept — and remove a member, which revokes their API keys and withdraws their shared credentials from the org. Removing an address that was only invited revokes the invitation instead. Both need the `members:write` scope, which only an org admin's key carries.\n\n**Knowledge.** A single free-form text/markdown file describing the org — who they are, what workflows they run, their goals. Shared context an agent reads before planning and updates as it learns. One file per organization; the write is an idempotent full replacement. No scope required — a key reads and writes only its own org's file."},{"name":"Workflows","description":"Full lifecycle management for workflows: create → define input columns → add actions → configure action inputs → add data rows → run → monitor results. Workflows are the core abstraction — each one defines a data processing pipeline with an input table, a chain of actions, and output data.\n\n**Sheets:** Each workflow has a main sheet (where `sheet_id` equals `workflow_id`) plus optional child sheets. Create child sheets with `POST /workflows/{workflow_id}/sheets`, inspect the full roster with `GET /workflows/{workflow_id}` (Get Workflow Overview), and delete child sheets with `DELETE /workflows/{workflow_id}/sheets/{sheet_id}`."},{"name":"Actions","description":"Configure the action chain on a workflow sheet: add actions, wire inputs with reference tokens, fetch dynamic field options, reorder, rename, and inspect outputs. All action endpoints live under `/api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/`."},{"name":"Environment Variables","description":"Organization-level values referenced from action fields as `{{env.<key>}}` and substituted per row when a workflow runs — API bases, shared keys, per-rep signatures. Use `GET /api/v1/environment-variables` to discover what exists and how to reference it.\n\n**Values are never returned by this API**, and they are read-only here: variables are created, edited and given values in the Floqer app.\n\n**Scopes.** `org` — one value for the whole organization. `user` — one value per member, resolved against the workflow's owner. `workflow` — one value per workflow, so it resolves only where a value is set; pass `workflow_id` to list just the variables usable there."},{"name":"Usage","description":"What the organization has to spend and what consumed it. `GET /api/v1/usage/credits` returns the credit balance; `GET /api/v1/usage/overview` returns spend for a window with the daily trend and top consumers; `GET /api/v1/usage/workflows` and `GET /api/v1/usage/sources` attribute that spend per workflow sheet and per source instance.\n\n**Windows.** Every breakdown takes `from` and `to` (ISO 8601, `from` inclusive, `to` exclusive) and defaults to the current calendar month in UTC.\n\n**What a key sees.** Reads need `billing:read`, which every role holds. A key that also holds `org:read` (admin only) reports the whole organization; any other key reports only the usage of the member who created it. The credit balance is organization-wide either way.\n\n**Runs and cache.** One run is one action call against one row. A run served from cache counts toward `runs` and `cache_hits` but spends no credits — `cache_savings` reports what it would have cost."},{"name":"Sources","description":"Data sources pull external data (e.g. from HubSpot, Slack) into Floqer, where it becomes available to your workflows. Each source is addressed by a `source_id` slug under `/api/v1/sources/{source_id}`.\n\nOperations per source type: **Preview**, **Create**, and **Get Source Field Options**. On created source instances: **Get Source Data**, **Sync**, and **Pause or Resume** (`PATCH .../status`). Per-source body shapes live in `/docs/source-detail/{source_id}.txt`."},{"name":"Agent","description":"TBD"},{"name":"Shortcuts","description":"Run published shortcuts programmatically. A shortcut wraps a workflow behind a typed input/output schema: discover shortcuts with List Shortcuts, read the required fields from `input_schema` via Get Shortcut, start a run with Run Shortcut, and poll Get Shortcut Run until a terminal status to read `output_data`."},{"name":"Campaigns","description":"Cold-outreach campaigns: a sequence of email and task steps that leads run through one at a time. Campaigns are independent of workflows — their own steps, leads and scheduler.\n\n**Build** — author the sequence: read the step catalog, register the lead fields your steps interpolate, add / update / delete steps, then publish. Steps are written to a draft and only the published sequence runs. The delay between two steps is a `wait` step managed for you — patch its `config` to change a gap.\n\n**Run** — add leads (up to 1000 per call; `email` is the dedup key, so retries are safe), list them by status, read one lead's step timeline, permanently delete leads, and read aggregate metrics.\n\n**Manage** — create and delete campaigns, list them, read one (including its `lead_schema`), read the sender selection, update settings (sending window, reply behaviour, bounce protection and `send_from`, changing only what you pass), and pause or resume sending.\n\nA campaign sends only when it has a published sequence **and** `state` is `active`."},{"name":"ackDB","description":"Direct access to your organization's ackDB deployment plus Floqer-owned durable segment-to-workflow sync management. Use **List ackDB Syncs** and the `/segments/{segment_id}/sync` routes to create, inspect, pause, resume, re-map, confirm, or delete a sync. These Floqer-owned routes use the standard `{ status, data }` envelope and public `input.<field>` mapping keys. Every other `/api/v1/ackdb/*` route maps 1:1 onto the ackDB API and retains ackDB's response envelope. Authenticate with your normal Floqer API key (`ackdb` scope); Floqer exchanges it for a short-lived ackDB token per request. Company and Contact relationship episode create/end/retract are available directly through the passthrough. The unified ackdb_company_relationship workflow action starts with a Company/Contact selector and provides the same episode mutations for row workflows, plus guarded company merge. Contact hierarchy reads remain available through the passthrough. Ingest and record updates retain their guarded workflow actions; legacy ackdb_merge_record clients remain supported. Relationship-type schema writes remain in the ackDB console/data-model UI."}],"externalDocs":{"description":"LLM-friendly flattened reference (single text file, served alongside this spec)","url":"/docs/api/llms-full.txt"}}