# ZergDesk token API

Base URL: `https://zergdesk.com`. ZergDesk supports sales, fundraising and custom
projects. This reference describes the supported **scoped bearer-token** API.
The complete CLI command guide is at https://zergdesk.com/docs/cli.md and the
sales model at https://zergdesk.com/docs/sales-crm.md.

## Authentication and tenant selection

Sign in with Zerg SSO and issue a token from **Account → API tokens**. Supply it
through a secret manager or secure credential form, never chat. An HTTP client
reads it from private configuration and sends the `Authorization` header with
scheme `Bearer`. Do not put credentials in URLs, shell arguments or logs. The
CLI handles this using `ZDESK_API_TOKEN`, `--token-env NAME` or a private 0600
`--token-file PATH`.

Tokens are bound to one workspace and optionally one project. They do not bypass
live membership, role, plan limits, reference validation or private-note access.
Available scopes are `workspace:read`, `workspace:write`, `crm:read`, `crm:write`.
A write scope also allows the corresponding read routes. Identity is available
with any valid token. Token issuance/revocation require a signed-in browser;
tokens cannot create other credentials.

For tenant API routes, add query `ws=WORKSPACE_SLUG` and, for a project-specific
operation, `projectId=PROJECT_ID`. `ws` is a workspace **slug**, not its ID.
`projectId` is a stable project **ID**, not its name or URL slug. Discover it
with `GET /api/workspace/WORKSPACE_SLUG/projects`. Workspace/project route
parameters use slugs. A token's workspace/project bindings reject other scopes.
The CLI adds these query selectors from its chosen profile or flags and rejects
conflicting explicit selectors.

JSON writes use `Content-Type: application/json`. Schemas reject unknown fields.
Calendar dates are valid `YYYY-MM-DD` strings or `null` where allowed. Activity
timestamps use ISO 8601 with an explicit offset. Reference IDs must belong to the
selected workspace; linked deals must also belong to the selected project.

## Supported bearer-token routes

`COLLECTION` below is one of `activities`, `tasks`, `resources`, `catalogs`,
`views`, `sequences`. Routes are relative to the base URL.

| Method | Path | Scope | Contract |
| --- | --- | --- | --- |
| GET | `/api/auth/me` | Valid token | `{user}` with bound workspace membership |
| GET | `/api/workspace` | `workspace:read` | `{workspaces}` within token binding |
| GET | `/api/workspace/:slug` | `workspace:read` | `{workspace,role}` including `plan`, `limits`, `usage` |
| GET | `/api/workspace/:slug/projects` | `workspace:read` | `{projects}` including IDs, slugs, types, settings |
| POST | `/api/workspace/:slug/projects` | `workspace:write` | Create project; workspace admin/owner required |
| PUT | `/api/workspace/:slug/projects/:projectSlug` | `workspace:write` | Patch project; workspace admin/owner required |
| DELETE | `/api/workspace/:slug/projects/:projectSlug` | `workspace:write` | Delete project; workspace admin/owner required |
| GET | `/api/pipeline` | `crm:read` | Complete authorized pipeline snapshot; project required |
| PUT | `/api/pipeline/firm` | `crm:write` | Create or patch an account/investor by `id` |
| PUT | `/api/pipeline/person` | `crm:write` | Create or patch a contact by `id` |
| PUT | `/api/pipeline/opportunity` | `crm:write` | Create or patch a deal by `id`; project required |
| DELETE | `/api/pipeline/opportunity` | `crm:write` | Delete deal, query `id`; project required |
| GET | `/api/pipeline/COLLECTION` | `crm:read` | Filtered, paginated collection |
| PUT | `/api/pipeline/COLLECTION` | `crm:write` | Create or patch a collection record |
| DELETE | `/api/pipeline/views` | `crm:write` | Delete saved view, query `id` |
| POST | `/api/pipeline/import` | `crm:write` | Batch partial upserts, optionally dry-run |
| GET | `/api/pipeline/duplicates` | `crm:read` | Candidate groups; query `entity=firms` or `people` |
| POST | `/api/pipeline/merge` | `crm:write` | Preview/apply merge; workspace admin, workspace-wide token |
| POST | `/api/pipeline/deck` | `crm:write` | Create deck and initial version; project required |
| PUT | `/api/pipeline/version` | `crm:write` | Add, update or delete a scoped deck version |
| POST | `/api/pipeline/deck-upload` | `crm:write` | Multipart PDF/PPTX upload into existing deck version |
| GET | `/api/pipeline/version-file` | `crm:read` | Version binary; query `versionId`, project required |
| GET | `/api/dataroom` | `crm:read` | List selected project's attachments |
| POST | `/api/dataroom` | `crm:write` | Multipart attachment upload; project required |
| GET | `/api/dataroom/:id/download` | `crm:read` | Authenticated attachment binary |
| POST | `/api/ztc/messages` | `crm:read` | Read-only CRM assistant, `{message}`; project required |
| GET | `/api/pipeline/record-workspace` | `crm:read` | Linked work; query `entity=firm\|person\|opportunity`, `id`, project required |
| GET | `/api/pipeline/my-day` | `crm:read` | Current-user task/reply/meeting/deal queues; project required |
| GET | `/api/pipeline/changes` | `crm:read` | Incremental attributed changes; query `cursor`, `limit`, optional `entity` |
| GET | `/api/pipeline/history` | `crm:read` | Record snapshots; query plural `entity`, `id`, optional `limit` |
| POST | `/api/pipeline/restore` | `crm:write` | Admin-only `{entity,id,historyId,expectedVersion}` recovery |
| GET/POST | `/api/pipeline/connections` | `crm:read`/`crm:write` | Owner's connections and capabilities / private-by-default setup |
| GET/PATCH/DELETE | `/api/pipeline/connections/:id` | `crm:read`/`crm:write` | Owner-only inspect / pause, visibility, credential rotation / disconnect |
| POST | `/api/pipeline/connections/:id/sync` | `crm:write` | Queue a durable job; no body required |
| GET | `/api/pipeline/connections/:id/items` | `crm:read` | Owner-only captured metadata and match results; `limit`, `offset`, `matchStatus` |
| GET | `/api/pipeline/connections/:id/jobs` | `crm:read` | Bounded job, retry and health history |

Public `GET /api/health` and `GET /api/version` require no token. All other routes
in the CLI's offline catalog are **not automatically bearer-token APIs**. In
particular, workspace creation/settings, membership management, token management,
SSO, billing, Gmail connection/sync, signatures, calendar, deck share-link
administration and analytics still require their appropriate browser session or
public-share flow. A token cannot be upgraded into one by calling `api`.

Project deletion and record merges affect stored data. Obtain the user's
authorization for the operation and retain an export before destructive work.

## Discovery and preflight

```sh
zdesk whoami
zdesk workspaces list
zdesk workspaces get WORKSPACE_SLUG
zdesk projects list WORKSPACE_SLUG
zdesk config set --workspace WORKSPACE_SLUG --project PROJECT_ID
zdesk pipeline get
zdesk crm export --include-sales --output desk-before.json
```

`workspace.limits` and `workspace.usage` expose project/member/firm capacity; `-1`
means unlimited. The default free plan is intentionally limited. Do not start an
import that exceeds capacity; arrange a suitable plan through the browser/admin
flow first. An export includes sensitive authorized CRM data; keep it private.

Project create accepts `{name,slug?,projectType?}` with `projectType` one of
`fundraise`, `sales`, `custom`. Project patch accepts `name`, `slug`,
`description`, `projectType`, `settings`. A name-only rename preserves the URL
slug. Settings merge **top-level keys**; a supplied array or nested object
replaces that key's value. Stage changes reject removal of stages used by deals.
Project-bound tokens cannot create another project or cross their binding.

## Accounts, contacts and deals

The three PUT routes accept **partial upserts**. Every request supplies `id`;
creating a firm/person additionally needs `name`, and creating a deal needs
`firmId`. Omitted fields preserve existing values on update. To clear a nullable
field send `null`; arrays are replaced, not appended. A response has
`{success:true,firm|person|opportunity,isNew}`. There is no generic firm/person
DELETE endpoint.

Record IDs are globally unique, up to 200 characters. Mint a random UUID once
per source record, optionally with an entity prefix, and persist the mapping
before creating it. Names, domains and emails are discovery keys rather than
identity. Cross-tenant references and ID reuse are rejected.

Common writable fields:

| Record | Fields |
| --- | --- |
| Firm/account | `name`, `website`, `kind`, `industry`, `companySize`, `arr`, `mrr`, `renewalDate`, `tags`, `productIds`, `segmentIds`, `notes`, `contacts`, `lastContact`, `obsidianPath`; investor fields include `checkSize`, `stage`, `type`, `status`, `tier`, `owner`, `introPath`, `inCurrentRaise` |
| Person/contact | `name`, `email`, `phone`, `company`, `firmId`, `role`, `linkedin`, `location`, `notes`, `privateNotes`, `lastContact`, `obsidianPath` |
| Opportunity/deal | `firmId`, `name`, `stageId`, `amount`, `closeDate`, `ownerUserId`, `ownerLabel`, `contactRoles`, `productIds`, `segmentIds`, `competitorIds`, `dealType`, `source`, `nextStep`, `nextStepDate`, `lostReason`, `tags`, `notes`, `kanbanOrder` |

`kind` is `investor`, `prospect`, `customer`, `partner`, `other` or empty.
`contactRoles` is an array of `{personId,role}`; `ownerUserId` names a workspace
member, while `ownerLabel` can label an external owner without granting access.
Amounts are finite nonnegative numbers or null. Current deal forecasts use USD.
`stageId` comes from project settings. Stage history and entry/activity timestamps
are server-owned observations; do not synthesize history in an importer.
Firm `status`, `tier`, `owner` and investor `stage` are currently workspace-level
fields, even when firms belong to multiple projects. `person.firmId` and
`firm.contacts` are independent links; keep both consistent where the workflow
uses both. Create base firms first, people next, then patch contact arrays and
create dependent deals. A dry run cannot create these dependencies.

People's `privateNotes` require the server's private-note admin access; workspace
ownership alone does not grant it. Unauthorized reads mask that field and writes
are rejected.

Example patch file for `zdesk opportunities update OPPORTUNITY_ID --data @deal.json`:

```json
{
  "nextStep": "Review evaluation results",
  "nextStepDate": "2026-11-05",
  "contactRoles": [{"personId": "EXISTING_PERSON_ID", "role": "champion"}]
}
```

A direct HTTP PUT includes `id` in the same JSON object. The CLI injects its
positional ID and rejects a conflicting payload ID.

## Record versions and synchronization

Core firms, people and opportunities plus all six sales collections carry
`version`, `updatedAt` and optional `provenance`. PUT accepts a nonnegative
`expectedVersion`: 0 means create only; a positive value must match the current
record. Mismatch returns HTTP 409 with `data:{code:"CRM_VERSION_CONFLICT",
currentVersion}`. Version checks and writes share one transaction. Omission
retains legacy upsert behavior. Use preconditions for agent/KB reconciliation;
blind legacy writes can still overwrite current fields. Read-only `version`
and `updatedAt` inputs are ignored. Identical writes preserve version/time.

Source provenance is `{system,externalId?,path?,runId?}`; IDs and paths refer to
the source without copying credentials or sensitive source contents. It is not
an authority claim. User/token actor IDs are set from authentication. Journaled
SQL writes and connector publications also produce change entries.

`GET changes` returns `{changes,nextCursor,hasMore}`; `limit` is 1–1,000. Entries
include `id` (history ID), `cursor`, plural `entity`, `recordId`, `operation`,
`version`, `updatedAt`, `actor:{userId,tokenId}`, `provenance`, and `record`.
Deletes return `record:null`. Follow `hasMore` and checkpoint `nextCursor` only
after processing. Cursors are opaque and bound to workspace, project and the
optional entity filter. Shared workspace records are visible under the same
rules as pipeline reads; project-specific entries stay in their project.

`GET history` returns `{history}` newest version first, with the same authorized
snapshots; `entity` and `id` are required. Existing records are baselined when
version tracking is introduced. This does not reconstruct earlier edits.
Snapshots follow current private-person-note permissions.

`POST restore` requires owner/admin membership and the current version. It
reapplies the selected history snapshot as a new attributed mutation, including
for a deleted record where current references/plan rules permit. Current
privacy checks still apply. It never alters past evidence. The CLI exposes
reviewable source patch plans and per-record apply reports on these APIs; see
`crm sync plan/apply` in the CLI guide. It does not install a vault watcher.

## Record workspace and personal queues

`GET record-workspace` takes singular entity, record ID, `limit` (1–100) and
`offset`. It returns the authorized record and separate paginated activities,
tasks, resources, meetings, meeting notes and explicitly targeted deck
engagement. Private capture is restricted to its owner and published equivalents
are deduplicated. A contact's firm alone is not evidence they attended a meeting.

`GET my-day` requires a selected project. Parameters include `assignment=mine`
(default) or `all`, `timeZone` (IANA, default UTC), optional `at` ISO instant,
`limit` (1–100), `offset`. It returns due/overdue open tasks, the next seven days
of meetings, owned open deals lacking next-step text or date, and unanswered
email candidates based on known sender/recipient chronology. Each queue is
paginated independently. Task dates are evaluated in the requested time zone.
Reply candidates are a heuristic rather than a thread-level obligation.

## Continuous communication capture

Connections belong to their owner and selected workspace/project. Even workspace
admins cannot read another member's private capture connection, credential or
items through these routes. CRM tokens retain their project binding; writes
also require a live non-viewer membership. Credentials are never returned.

Create body: `{provider,label,projectId?,origin?,resourceId?,accountId?,token?,
visibility?,intervalMinutes?}`. Providers are `zmail`, `zcal`, `zergmeeting`,
`gmail`. Visibility defaults to `private`; interval is 5–1,440 minutes (default
15). Supply upstream secrets through a secure client. The first-party CLI accepts
them only via credential environment/file flags, separately from metadata JSON.

ZMail (`https://zerg-mail.com`) uses a mailbox capability for inbox/sent/archive metadata.
ZCal (`https://zergcalendar.com`) uses an `org:read` API token plus a calendar
`resourceId` and reconciles a 30-day past/90-day future window. ZergMeeting
(`https://zergmeet.com`) uses `org:read` and a metadata cursor feed, without
recordings or transcripts. Gmail uses an existing OAuth-connected `accountId`
owned by the caller, without a supplied token; Google OAuth remains a browser
workflow. Origins are limited to configured trusted services; redirects are
refused. Provider credentials are encrypted at rest.

Create/explicit sync enqueue a persisted job. The worker leases one bounded
provider page, resumes cursors, retries transient errors with backoff and
reports safe health/error codes. `GET items` returns metadata, matching links,
publication state, counts and pagination; `GET jobs` returns recent outcomes.
Exact normalized contact emails drive matching. Multiple candidate accounts or
duplicate contact emails are ambiguous; deal association requires a single
candidate in the project. Missing/ambiguous matches remain private review items.

`PATCH` accepts `label`, `enabled`, `visibility`, `intervalMinutes`, or rotated
`token`. Workspace publication is explicit and checks human edits under a lock;
human changes produce a conflict instead of being overwritten. Reverting to
private or disconnecting withdraws generated shared metadata. `DELETE` disables
jobs and erases the stored credential, retaining human notes and audit evidence.
It does not delete the upstream account or disconnect Google OAuth itself. Previously published metadata remains in CRM history and change feeds; visibility changes do not retroactively purge shared evidence.
Capture never sends email, invitations or private recordings to another app.

## Activities, tasks and sales resources

Collection GET supports `id`, `firmId`, `opportunityId`, `personId`, `kind`,
`status`, `limit`, `offset`. Default/max `limit` is 1000. The response contains
`{COLLECTION:[...],pagination:{limit,offset,hasMore}}`; follow pages until
`hasMore` is false. PUT accepts an optional `id` (generated if absent), merges
existing records, and returns `{success:true,SINGULAR:record}`. Supply persistent
IDs for repeatable ingestion. Most collections require a project; catalog entries
are workspace-wide. Links use `firmId`, `opportunityId`, `personId` or null.

| Collection | Required on create | Additional fields |
| --- | --- | --- |
| activities | `kind`: email/call/meeting/note; `subject` | `body`, `occurredAt` ISO timestamp, `direction`: inbound/outbound/internal, `messageId`, `from`, `to` array |
| tasks | `title` | `notes`, `dueDate`, `status`: open/in-progress/completed/archived, `assignee` |
| resources | `kind`: attachment/contract/invoice/case-study/collateral; `name` | HTTP(S) `url`, `markdown`, `mimeType`, `status`: draft/active/signed/paid/archived, `amount`, 3-letter `currency`, `dueDate` |
| catalogs | `kind`: product/segment/competitor; `name` | `description`, HTTP(S) `website`, `status`: active/archived |
| views | `name`; `entity`: accounts/opportunities | `filters`, `columns`, `recordIds` |
| sequences | `name` | `status`: draft/active/paused/completed/archived, `steps`, `enrollments` |

Catalog kind cannot change after creation. View `filters` keys are `search`,
`tags`, `stageIds`, `ownerIds`, `firmIds`, `productIds`, `segmentIds`,
`competitorIds`. Sequence steps have
`id`, `kind`, `subject`, `body`, `waitDays`; enrollments have `personId`, optional
`firmId`, `status`, `stepIndex`, `nextDueDate`. Activity `messageId` deduplicates
repeat email ingestion. Sequence writes never send messages. Contract/invoice
resources are CRM metadata, not payment collection or automatic signing.

Example meeting file for `zdesk activities create --data @meeting.json`:

```json
{
  "id": "PERSISTED_RANDOM_ACTIVITY_ID",
  "kind": "meeting",
  "subject": "Evaluation review",
  "occurredAt": "2026-11-05T10:00:00-08:00",
  "firmId": "EXISTING_FIRM_ID",
  "opportunityId": "EXISTING_OPPORTUNITY_ID",
  "body": "Agreed to follow up on the evaluation plan."
}
```

## Batch import, duplicates and merge

`POST /api/pipeline/import` accepts:

```json
{
  "entity": "firms",
  "records": [{"id": "PERSISTED_RANDOM_FIRM_ID", "name": "Example Account"}],
  "dryRun": true
}
```

Supported entities are `firms`, `people`, `opportunities`, with 1–500 records per
request. Results include `succeeded`, `failed` and per-record `index`, `id`,
`success`, `isNew` or `error`. A request may return HTTP 200 with failed records;
inspect the results. The batch is not all-or-nothing. Dry runs validate current
state and capacity but do not persist dependencies for later records or batches.

The CLI accepts JSON arrays, `{records:[...]}` JSON, or CSV with field-name
headers using `crm import ENTITY --file PATH [--dry-run]`. It splits batches and
returns exit 4 if any record fails. Files are bounded to 8 MiB. Read the CLI guide
for CSV null/blank and array conventions. Import firms before dependent people,
then deals. After ambiguous failure, inspect remote IDs before retrying.

Duplicates are exact normalized name/domain or email candidates. Merge body:
`{entity:"firms"|"people",sourceId,targetId,dryRun:true}`. Preview first; only
`dryRun:false` applies. Applying preserves target scalar values, fills blanks,
combines notes/lists, moves references and removes the source transactionally.
It requires workspace admin rights and a workspace-wide token; people merges
also require private-note access. A read-only reconciliation is not merge
permission.

## Decks and attachments

Create a deck with `{id?,versionId?,name,googleSlidesUrl?,obsidianPath?}`. Persist
explicit random deck/version IDs for safe retries. The response is
`{success:true,deckId,versionId}`. Version mutation body is
`{deckId,action:"add"|"update"|"delete",version:{id,...}}`. Version fields include
`version`, `name`, `status` (current/draft/archived), `googleSlidesUrl`, `notes`,
`changelog`, `parentVersionId`, `persona`. The current version cannot be deleted
until another is promoted. File uploads require an existing scoped deck/version.

```sh
zdesk decks create --data @deck.json
zdesk decks version-update --data @version.json
zdesk decks upload DECK_ID VERSION_ID --file deck.pdf
zdesk dataroom upload --file case-study.md --data @attachment.json
```

Deck upload is multipart with `deckId`, `versionId`, `file` and accepts PDF/PPTX
from 1 byte to 32 MiB. PDF is rendered server-side; PPTX conversion depends on a
configured Google integration and may return 502. Dataroom multipart fields are
`file`, optional `id`, `description`, `category`, with the same size limit and
409 on reused ID. The CLI builds the multipart request; generic `api` sends JSON.

Link an uploaded attachment to an account/deal using a `resources` record with
its authenticated absolute download URL. That URL still requires authorized
access. Uploading does not create a public share link. Share administration and
deck analytics remain browser-session workflows.

## Agent action queues and concurrent CRM work

Read [the queue operating contract](https://zergdesk.com/docs/action-queues.md)
for payloads, identity and handoffs. All these routes require a selected project.

| Method | Path | Access |
| --- | --- | --- |
| GET / POST | `/api/pipeline/agents` | CRM read / write, token-bound registration |
| PATCH | `/api/pipeline/agents/:id` | CRM write, agent token or owner/admin |
| POST | `/api/pipeline/agents/:id/heartbeat` | CRM write, own registered agent |
| GET / POST | `/api/pipeline/queues` | CRM read / write plus queue policy rights |
| PATCH | `/api/pipeline/queues/:id` | Queue owner or workspace admin, versioned |
| GET / POST | `/api/pipeline/actions` | CRM read / write; registered token for submissions |
| GET / PATCH | `/api/pipeline/actions/:id` | Read / versioned creator or human-owner revision |
| POST | `/api/pipeline/actions/:id/transition` | Versioned operation with actor/state checks |
| GET | `/api/pipeline/actions/:id/events` | Attributed history, bounded limit/offset |
| GET | `/api/pipeline/action-events` | Durable scope-bound cursor feed |
| POST | `/api/pipeline/notes` | CRM write, immutable idempotent note append |
| POST | `/api/pipeline/tags` | CRM write, atomic account/deal tag deltas |

Agents bind an existing API token to a project identity; no additional secrets
are minted. Queue `ownerUserId` is its human approver; `executorAgentId` names
the channel agent. Action creation requires `queueId,idempotencyKey,type,recipient`
and supports exact `subject,body,targetUrl` plus linked CRM IDs. Responses expose
`state,version,revision,payloadHash,permissions` and approval/receipt fields.
PATCH/transition require a positive `expectedVersion`. Transition body is
`{operation,expectedVersion,reason?,leaseToken?,receipt?}`. Operations are submit,
review, request-changes, approve, reject, claim, complete, fail, cancel, release.
Reasons are required when returning work or resolving uncertainty. Only the
queue owner's browser session may approve/reject/release. Tokens cannot perform
those decisions. Claims require the designated executor, unchanged approval/policy
and a live token; they return a short-lived lease. Completion requires that lease
and `receipt:{detail,externalId?,url?}`. Expiry/fail enters uncertainty, never an
automatic send retry. Release with a verified-delivery receipt marks done;
release without one resets to draft for fresh approval.

Lists accept limit 1–200 and offset. Actions additionally accept queueId/state.
The feed returns `{events,cursor,hasMore}`; cursors cannot cross projects.
An event is a notification, not authority to act on a stale action snapshot.

Note append body is `{entity:"firms"|"people"|"opportunities",id,body,
clientMutationId,subject?,occurredAt?}`; `{activity,replayed}` retains attribution.
Tag delta body is `{entity:"firms"|"opportunities",id,add?:string[],remove?:string[],expectedVersion?}`.
Empty deltas/overlap are rejected. No-op deltas preserve the record version.
Firms add `ownerAgentId` and `buyingSignalUrl`; people add `connectionDegree`
(`""|"1st"|"2nd"|"3rd"|"out-of-network"`), `outreachStatus` and `buyingSignalUrl`.
URLs must be HTTP(S) or empty. Agent ownership requires an active registered
project agent with live workspace/token access.

## Error handling and export guarantees

Validation errors use HTTP 400 with field diagnostics; 401 indicates missing,
expired or invalid credentials; 403 covers scope/role/tenant/plan refusal; 409
covers identity conflicts or protected state. Do not treat all 403s as a need
for broader credentials: read the server's message. CLI errors expose a meaningful
`error.message` and bounded structured `details`, redact active credentials and
strip server stacks.

Mutations have no automatic retries. An export is a read-only snapshot. Use explicit `expectedVersion` writes and
the incremental `/api/pipeline/changes` cursor feed for conflict-aware sync.
Legacy writes without a version remain blind upserts for compatibility. Preserve
user edits on 409 and keep source mappings updated with returned versions.

`crm export --include-sales --output NEW_FILE.json` follows every sales page and
creates the private bundle only after all reads succeed. Existing files and
symlinks are never overwritten. `ztc ask` can send selected CRM context to the
configured model provider; use it for requested analysis, not connectivity tests
with private data.

## Campaign cohorts, funnels, observations and analyst reports (CLI 0.5.0)

These routes require `ws`, `projectId` and the corresponding `crm:read` or
`crm:write` scope. All linked IDs are validated inside the selected tenant and
project. The full measurement and JSON guide is
https://zergdesk.com/docs/campaign-analytics.md.

| Method | Path | Contract |
| --- | --- | --- |
| GET | `/api/pipeline/campaigns` | `{campaigns,pagination}`; optional `id`, `status`, `segmentId`, `ownerId` |
| PUT | `/api/pipeline/campaigns` | Create/partial update; `expectedVersion` required for existing records |
| POST | `/api/pipeline/campaigns/members` | `{id,expectedVersion,members,mode:"preview"\|"append"}`; preview is default |
| POST | `/api/pipeline/campaigns/launch` | `{id,expectedVersion}` pins a published funnel revision and cohort |
| POST | `/api/pipeline/campaigns/clone` | `{id,expectedVersion,newId?,name?}` creates a new draft cohort |
| POST | `/api/pipeline/campaigns/seed` | `{mode:"preview"\|"apply",members?:[{campaignName,member}]}` seeds five briefs and outbound journey |
| GET | `/api/pipeline/funnels` | `{funnels,pagination}`; optional `id`, `campaignId`, `revision` |
| PUT | `/api/pipeline/funnels` | Create/partial update draft graph; existing write requires `expectedVersion` |
| POST | `/api/pipeline/funnels/validate` | Validate a graph's typed predicates, references and reachability |
| POST | `/api/pipeline/funnels/publish` | `{id,expectedVersion}` creates immutable published revision |
| GET | `/api/pipeline/observations` | `{observations,pagination}`; optional campaign, type and cutoff filters |
| POST | `/api/pipeline/observations` | Immutable source-identified evidence; corrections use `supersedesId` |
| GET | `/api/pipeline/analytics/overview` | Campaigns, funnel analyses, distinct current counts, coverage and approval backlog |
| GET | `/api/pipeline/analytics/funnel` | Historical/snapshot result for selected campaign/funnel and published revision |
| GET | `/api/pipeline/analytics/drilldown` | Paginated contributing entities and evidence for `nodeId` |
| GET | `/api/pipeline/analytics/export` | `{records,pagination,watermark}`; `collection=campaigns\|funnels\|observations\|funnelRevisions\|identityAliases\|entities\|analyses` |
| GET | `/api/pipeline/reports` | `{reports,pagination}`; optional `id` selects `{report}` |
| POST | `/api/pipeline/reports/generate` | `{campaignId?,idempotencyKey?}` queues a report; returns `{report,success:true}` |
| GET | `/api/pipeline/reports/settings` | `{settings}` including version and generation limits |
| PUT | `/api/pipeline/reports/settings` | Owner/admin only; patch enabled/limits with `expectedVersion` |

Collection lists accept `limit` (1–1000, default 100) and `offset`.
`pagination:{limit,offset,hasMore}` describes continuation. Campaign/funnel GET
with `id` uses the same route as list. Archive a campaign by PUT
`{id,status:"archived",expectedVersion}`. A published funnel revision remains
available even after editing its draft. A launched cohort accepts append-only
batches, preserving prior attribution; clone it for a new experiment.

An enrollment has CRM links `personId?`, `firmId?`, `opportunityId?`, an actual
`enrolledAt` or null, optional `ownerId`, `sourceId` and `batchId`. At least one
CRM link is required. New proof-point approvals require a signed-in human workspace owner/admin and
a source URL; scoped tokens may preserve existing approvals but cannot grant
sharing permission. A campaign has authored `brief`, segment references,
optional owner/start date and a pinned funnel revision. Counting unit belongs to
the funnel: `contacts`, `accounts` or `deals`; linked units are not silently
expanded to every record associated with an account.

Observation writes include `type`, `occurredAt` (timestamp or null), `source`,
`sourceId`, CRM references and optional campaign/enrollment/channel/thread/source URL.
`source` plus `sourceId` provides idempotency: matching replay returns the
existing record; conflicting contents return 409. Correct through a new source
identity and `supersedesId`; `retracted:true` withdraws incorrect evidence without
removing audit history. Actor and recorded time come from the server. Private
capture metadata is excluded from shared results.

Analytics filters include `campaignId`, `funnelId`, `revision`, `segmentId`,
`ownerId`, `channel`, `asOf`, `recordedThrough`, `cohortFrom`, `cohortThrough`.
`asOf` bounds actual event time; `recordedThrough` bounds known evidence at an
ingestion watermark. Unknown dates are coverage gaps, not zero activity.
Current predicates contribute only to snapshot results. Historical results retain
unique reach, eligible conversion, repeat visits/transitions and elapsed time.
A verified brand-new deal created through direct Desk opportunity PUT records
`opportunity.created` transactionally and idempotently. The event means Desk
deal creation, not qualification. Bulk import/sync paths and known import/sync
provenance are excluded; historical records need explicit dated observations.
A queue approval or delivery receipt cannot establish recipient acceptance,
qualified reply or a held meeting. Reports are explanatory artifacts, never
outbound authorization or automated edits to observed facts.

`crm export --include-campaigns` adds complete campaigns, funnels, observations,
reports, immutable published funnel revisions, merged-record identity aliases and analyst settings. Combine with `--include-sales` for workflow
collections. `crm changes` supports subsequent incremental pulls; checkpoint its
scope-bound cursor after processing the page. Keep record versions and source
IDs in knowledge-base mappings; never retry a 409 blindly.


## Sales intelligence 0.6.0

GET/PUT `prospects`, GET/PUT `outreach/settings`, GET `analytics/acquisition`
provide project prospect state, versioned status definitions, current/as-of
snapshots, compareWith deltas and groupBy lane/segment/ownerGroup rollups.
GET/POST `sources`, GET `sources/bindings`, GET `reconciliations` and
`reconciliations/conflicts`, POST `reconciliations/plan|apply|ack|resolve` expose
the source-v2 handshake. POST `prospects/reconcile` previews/applies explicit
legacy mappings. All paths are relative to `/api/pipeline` and require normal
workspace/project scope and crm:read/write.

GET/PUT `relationships`, `chosen-paths`, `intro-requests`, `conversations`,
`signals`, `proofs`; GET `relationships/paths|reach`; POST `relationships/import`;
GET `intelligence/changes` expose ACL-aware records. PUT requires expectedVersion
(0 for create), preserves omitted fields and returns a singular record. GET
returns records/pagination. Relationship imports require previewToken and
per-row reviewed selection. A human browser must authorize publication or proof
approval; write tokens do not acquire that authority.

GET `opportunities?includeArchived=true` lists archived deals; POST
`opportunities/archive` previews/applies version-checked archive or restore.
POST `actions/approve-batch` is browser-only, with per-item id, expectedVersion,
expectedPayloadHash and expectedQueueVersion. It is not token-allowlisted.

See [source protocol](https://zergdesk.com/docs/prospect-sync.md),
[introductions](https://zergdesk.com/docs/introductions.md) and the generated
route catalog for exact schemas and named CLI commands.
