---
title: "Accounts API reference"
canonical_url: https://zentrik.ai/docs/api/accounts
markdown_url: https://zentrik.ai/docs/api/accounts.md
last_reviewed: 2026-10-05
---

# Accounts API reference

Manage the workspace people and Accounts that connect product engagement across calls, signals, and insights.

- Human reference: https://zentrik.ai/docs/api/accounts
- Complete Markdown index: https://zentrik.ai/docs/api/index.md
- Base URL: `https://zentrik.ai/api`
- Authentication: `Authorization: Bearer YOUR_API_KEY`
- Shared pagination and rate limits: https://zentrik.ai/docs/api/index.md#shared-conventions

## GET /external/v1/people — List people

Operation ID: `list-people`

List the workspace people directory across all Accounts, including unassigned people created from public email addresses. Each result includes its linked signal count.

Required API key scopes: `accounts:read`

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `q` | string | No | Search name, email, role, Account name, or Account domain. |
| `limit` | number | No | Maximum results, from 1 to 100. |
| `offset` | number | No | Number of people to skip. |

### Example request

```curl
curl -X GET 'https://zentrik.ai/api/external/v1/people?q=buyer' \
  -H 'Authorization: Bearer YOUR_API_KEY'
```

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 200 | Success | Workspace people were retrieved. | PeoplePage |

### Example response

```json
{
  "items": [
    {
      "id": "person-uuid",
      "name": "Primary product contact",
      "email": "buyer@example.com",
      "role": "VP Product",
      "accountId": "account-uuid",
      "account": {
        "id": "account-uuid",
        "name": "Example",
        "domain": "example.com"
      },
      "signalCount": 4
    }
  ],
  "totalRecords": 1,
  "limit": 50,
  "offset": 0,
  "hasMore": false
}
```

---

## POST /external/v1/people — Create a person

Operation ID: `create-person`

Create a workspace person. accountId is optional, so public-email and not-yet-matched people can exist before Account assignment.

Required API key scopes: `accounts:write`

### JSON request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | Yes | Person display name. |
| `email` | string | No | Email used for deterministic matching on later calls. |
| `role` | string | No | Role or title. |
| `accountId` | uuid \| null | No | Account assignment, or null for unassigned. |

### Example request

```json
{
  "name": "Primary product contact",
  "email": "buyer@example.com",
  "role": "VP Product",
  "accountId": "account-uuid"
}
```

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 201 | Created | The person was created. | Person |
| 409 | Conflict | A person with the same workspace email already exists. | — |

---

## PATCH /external/v1/people/:id — Update or assign a person

Operation ID: `update-person`

Update a person or assign an unassigned person to an Account. Existing signal history stays linked to the person.

Required API key scopes: `accounts:write`

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | uuid | Yes | The person UUID. |

### JSON request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | No | Updated display name. |
| `email` | string | No | Updated email. |
| `role` | string | No | Updated role or title. |
| `accountId` | uuid \| null | No | New Account assignment, or null to unassign. |

### Example request

```curl
curl -X PATCH https://zentrik.ai/api/external/v1/people/person-uuid \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"accountId":"account-uuid"}'
```

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 200 | Updated | The person was updated. | Person |

---

## DELETE /external/v1/people/:id — Delete a person

Operation ID: `delete-person`

Delete a workspace person and its signal links. Signals and Accounts are not deleted.

Required API key scopes: `accounts:write`

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | uuid | Yes | The person UUID. |

### Example request

```curl
curl -X DELETE https://zentrik.ai/api/external/v1/people/person-uuid \
  -H 'Authorization: Bearer YOUR_API_KEY'
```

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 200 | Deleted | The person was deleted. | — |

---

## GET /external/v1/accounts — List all accounts

Operation ID: `list-accounts`

List accounts in the current workspace. This is the main lookup endpoint for CRM automation before transcript ingestion.

Required API key scopes: `accounts:read`

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | number | No | Maximum number of accounts to return. |
| `offset` | number | No | Number of accounts to skip. |
| `q` | string | No | Case-insensitive search across name, domain, website, and externalId. |
| `name` | string | No | Exact account name match (case-insensitive). |
| `domain` | string | No | Exact domain or website match (case-insensitive). |
| `externalId` | string | No | Stable client-side identifier such as crm-example-account. |
| `updatedSince` | iso-date | No | Only accounts whose own fields changed at or after this ISO-8601 timestamp, oldest change first. Pass the latest updatedAt you have received to sync incrementally, and dedupe by id. Changes made only to contacts or source links may not change an account’s updatedAt. |

### Example request

```curl
curl -X GET 'https://zentrik.ai/api/external/v1/accounts?externalId=crm-example-account' \
  -H 'Authorization: Bearer YOUR_API_KEY'
```

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 200 | Success | Accounts were successfully retrieved. | Array<Account> |

### Example response

```json
[
  {
    "id": "account-uuid",
    "name": "Example Account",
    "domain": "example.com",
    "website": "https://example.com",
    "lifecycleStage": "TRIAL",
    "externalId": "crm-example-account",
    "workspaceId": "workspace-uuid",
    "contactIds": [
      "contact-1"
    ],
    "insightIds": [
      "insight-1"
    ],
    "opportunityIds": [
      "opportunity-1"
    ],
    "ideaIds": [
      "idea-1"
    ],
    "createdAt": "2026-04-09T10:00:00Z",
    "updatedAt": "2026-04-09T10:00:00Z"
  }
]
```

---

## GET /external/v1/accounts/duplicate-candidates — Find duplicate accounts

Operation ID: `find-duplicate-accounts`

Find likely duplicate Account groups in the workspace before a merge. Candidates are grouped by shared domain, normalized name, or legal name and ordered by confidence. This operation does not change data; a candidate is evidence for review, not an automatic merge decision.

Required API key scopes: `accounts:read`

### Example request

```curl
curl -X GET https://zentrik.ai/api/external/v1/accounts/duplicate-candidates \
  -H 'Authorization: Bearer YOUR_API_KEY'
```

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 200 | Success | Likely duplicate Account groups were retrieved. | Array<AccountDuplicateCandidate> |

### Example response

```json
[
  {
    "score": 100,
    "reason": "domain",
    "accounts": [
      {
        "id": "account-uuid-1",
        "name": "Example",
        "domain": "example.com",
        "lifecycleStage": "ACTIVE"
      },
      {
        "id": "account-uuid-2",
        "name": "Example Inc.",
        "domain": "example.com",
        "lifecycleStage": "TRIAL"
      }
    ]
  }
]
```

---

## GET /external/v1/accounts/:id — Get one account

Operation ID: `get-account`

Retrieve one account from the current workspace.

Required API key scopes: `accounts:read`

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | uuid | Yes | The backend account UUID. |

### Example request

```curl
curl -X GET https://zentrik.ai/api/external/v1/accounts/account-uuid \
  -H 'Authorization: Bearer YOUR_API_KEY'
```

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 200 | Success | The account was successfully retrieved. | Account |
| 404 | Not Found | No account found with the provided id. | — |

### Example response

```json
{
  "id": "account-uuid",
  "name": "Example Account",
  "domain": "example.com",
  "website": "https://example.com",
  "lifecycleStage": "TRIAL",
  "externalId": "crm-example-account",
  "workspaceId": "workspace-uuid",
  "contactIds": [
    "contact-1"
  ],
  "insightIds": [
    "insight-1"
  ],
  "opportunityIds": [
    "opportunity-1"
  ],
  "ideaIds": [
    "idea-1"
  ],
  "createdAt": "2026-04-09T10:00:00Z",
  "updatedAt": "2026-04-09T10:00:00Z"
}
```

---

## POST /external/v1/accounts — Create an account

Operation ID: `create-account`

Create an account in the current workspace. CRM agents typically use `externalId` as the durable bridge between local records and Zentrik accounts. Manage contacts with the account contacts endpoints after the account exists.

Required API key scopes: `accounts:write`

### JSON request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | Yes | Account display name. |
| `externalId` | string | No | Stable client-side identifier such as crm-example-account. |
| `domain` | string | No | Primary company domain. |
| `website` | string | No | Canonical website URL. |
| `industry` | string | No | Industry or vertical label. |
| `companySize` | string | No | Company size bucket. |
| `lifecycleStage` | string | No | Canonical Zentrik lifecycle stage: LEAD, TRIAL, ACTIVE, CHURNED, or PARTNER. |
| `dataClassification` | string | No | Account type tag: EXTERNAL (default), INTERNAL, or TEST. Tags organize and filter accounts; they do not change revenue totals. |
| `notes` | string | No | Supplemental operator or CRM notes. |

### Example request

```json
{
  "name": "Example Account",
  "externalId": "crm-example-account",
  "domain": "example.com",
  "website": "https://example.com",
  "industry": "B2B software",
  "companySize": "11-50",
  "lifecycleStage": "TRIAL",
  "notes": "Primary contact evaluates roadmap fit."
}
```

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 201 | Created | The account was successfully created. | Account |

### Example response

```json
{
  "id": "account-uuid",
  "name": "Example Account",
  "externalId": "crm-example-account",
  "domain": "example.com",
  "website": "https://example.com",
  "industry": "B2B software",
  "companySize": "11-50",
  "lifecycleStage": "TRIAL",
  "workspaceId": "workspace-uuid",
  "contactIds": [],
  "insightIds": [],
  "opportunityIds": [],
  "ideaIds": [],
  "createdAt": "2026-04-09T10:00:00Z",
  "updatedAt": "2026-04-09T10:00:00Z"
}
```

---

## PATCH /external/v1/accounts/:id — Update an account

Operation ID: `update-account`

Update an existing account in the current workspace. Contact arrays are not accepted here; use the dedicated contact endpoints for people and roles.

Required API key scopes: `accounts:write`

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | uuid | Yes | The backend account UUID. |

### JSON request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | No | Updated display name. |
| `externalId` | string | No | Updated stable client-side identifier. |
| `domain` | string | No | Updated primary company domain. |
| `website` | string | No | Updated canonical website URL. |
| `industry` | string | No | Updated industry or vertical label. |
| `companySize` | string | No | Updated company size bucket. |
| `lifecycleStage` | string | No | Updated canonical Zentrik lifecycle stage. |
| `dataClassification` | string | No | Updated account type tag: EXTERNAL, INTERNAL, or TEST. |
| `notes` | string | No | Updated operator or CRM notes. |

### Example request

```curl
curl -X PATCH https://zentrik.ai/api/external/v1/accounts/account-uuid \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"lifecycleStage":"ACTIVE"}'
```

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 200 | Updated | The account was successfully updated. | Account |

---

## DELETE /external/v1/accounts/:id — Delete an account

Operation ID: `delete-account`

Delete an account from the current workspace.

Required API key scopes: `accounts:delete`

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | uuid | Yes | The backend account UUID. |

### Example request

```curl
curl -X DELETE https://zentrik.ai/api/external/v1/accounts/account-uuid \
  -H 'Authorization: Bearer YOUR_API_KEY'
```

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 200 | Deleted | The account was successfully deleted. | — |

---

## POST /external/v1/accounts/:id/merge — Preview or merge duplicate accounts

Operation ID: `merge-accounts`

Preview or execute a destructive duplicate Account merge. The path Account survives; source Accounts are folded into it and deleted. Start with dryRun set to true, review moved records and field choices, resolve every blocking conflict, then repeat with dryRun set to false. Executed merges retain a bounded recovery journal (10,000 rows, 4 MiB). Undo is available for seven days only when the supported graph is unchanged; unsupported graphs block automatic undo. Some unsupported references or journal limits block the merge itself. Raw recovery snapshots are never returned by the public API.

Required API key scopes: `accounts:delete`

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | uuid | Yes | The Account UUID that must survive the merge. |

### JSON request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `sourceAccountIds` | uuid[] | Yes | One or more duplicate Account UUIDs to fold into the survivor and delete. |
| `dryRun` | boolean | No | Set true to return the merge plan without writing. Defaults to false. |
| `fieldOverrides` | object | No | Explicit survivor field values to use when duplicate records disagree. |

### Example request

```curl
curl -X POST https://zentrik.ai/api/external/v1/accounts/account-uuid-1/merge \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"sourceAccountIds":["account-uuid-2"],"dryRun":true}'
```

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 200 | Success | A dry run returns the merge plan. An executed merge returns the surviving Account and mergeOperationId for recovery review. | AccountMergePreview \| (Account & { mergeOperationId: uuid }) |
| 400 | Invalid request | The source list is empty, includes the survivor, or contains an invalid Account reference. | — |
| 404 | Not found | The survivor or a source Account is outside the API-key workspace or does not exist. | — |
| 409 | Blocking conflict | A source identity or integration binding must be resolved before the Accounts can be merged. | — |

### Example response

```json
{
  "survivorId": "account-uuid-1",
  "survivorName": "Example",
  "losers": [
    {
      "id": "account-uuid-2",
      "name": "Example Inc."
    }
  ],
  "moves": {
    "signals": 3,
    "insights": 2,
    "ideas": 0,
    "opportunities": 1,
    "contacts": 2
  },
  "totalMoves": 8,
  "fieldResolutions": [],
  "conflicts": [],
  "blocking": false
}
```

---

## GET /external/v1/accounts/:id/company-context — Get account company context

Operation ID: `get-account-company-context`

Read the Account, ancestors, immediate children, source parent proposals, and company measures. Children remain distinct entities, not a physical-location count. Revenue and Evidence remain Account-scoped unless explicitly requested through another supported workflow. This uses the same company-context service as the application.

Required API key scopes: `accounts:read`

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | uuid | Yes | The Account UUID in the API-key workspace. |
| `offset` | integer | No | Child offset from 0 to 100000; defaults to 0. Pages contain at most 50 children. Follow nextOffset until null. |

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 200 | Success | Returns account, ancestors, children, childCount, offset, nextOffset, sources, sourceParent, measures, and warnings. | AccountCompanyContext |
| 400 | Invalid offset | Offset is outside the supported integer range. | — |
| 404 | Not found | The Account is not in this workspace. | — |

---

## PATCH /external/v1/accounts/:id/parent — Set account parent

Operation ID: `set-account-parent`

Set a reviewed immediate parent in this workspace, detach with parentAccountId null, or apply the resolved source parent with useSource true. Source family membership alone does not prove a parent relationship. Loops and invalid relationships are rejected. Uses the application hierarchy service.

Required API key scopes: `accounts:write`

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | uuid | Yes | The Account UUID in the API-key workspace. |

### JSON request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `parentAccountId` | uuid \| null | No | Explicit parent Account, or null to detach. Do not combine with useSource true. |
| `useSource` | boolean | No | Set true to use the uniquely resolved source parent instead of a manual parent. |

### Example request

```json
{
  "useSource": true
}
```

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 200 | Updated | Returns refreshed company context. | AccountCompanyContext |
| 409 | Conflict | The source parent is unresolved, conflicting, or invalid, or the requested hierarchy would be unsafe. | — |

---

## GET /external/v1/accounts/:id/measures — Get account measures

Operation ID: `get-account-measures`

Read Account and company operational measures through the same service as the application. Null means unknown, not zero. Self readings describe one Account; company_total readings belong only to the current company root and replace member sums for that measure. Partial or conflicting totals are identified rather than presented as complete.

Required API key scopes: `accounts:read`

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | uuid | Yes | The Account UUID in the API-key workspace. |

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 200 | Success | Returns account and company measure summaries, knownValue, coverage, status, basis, and source readings. | AccountMeasuresContext |
| 409 | Conflict | The hierarchy or measure scope exceeds safe limits; no partial total is substituted. | — |

---

## PATCH /external/v1/accounts/:id/measures — Record an account measure

Operation ID: `upsert-account-measure`

Record a dated, source-attributed operational measure. Repeating the same Account, key, unit, scope, source, and date updates that reading. Use null for an explicitly unknown value. Company totals can be recorded only on the current company root; this does not change ARR or copy a family total to each child.

Required API key scopes: `accounts:write`

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | uuid | Yes | The Account UUID in the API-key workspace. |

### JSON request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `key` | string | Yes | Stable lowercase measure key, up to 80 characters. |
| `label` | string | Yes | Display label, up to 120 characters. |
| `unit` | string | Yes | Measure unit, up to 40 characters; for example locations. |
| `value` | number \| null | Yes | Finite numeric value, or null for unknown. |
| `scope` | string | Yes | self or company_total. The latter is root-only. |
| `observedAt` | date | Yes | Observation date in YYYY-MM-DD format. |
| `sourceKey` | string | Yes | Source identity, up to 255 characters. |
| `sourceRecordId` | string | Yes | Exact source record ID, up to 255 characters. |
| `sourceUrl` | url \| null | No | Optional HTTP or HTTPS evidence URL. |

### Example request

```json
{
  "key": "locations",
  "label": "Locations",
  "unit": "locations",
  "value": null,
  "scope": "self",
  "observedAt": "2026-10-03",
  "sourceKey": "example:inventory",
  "sourceRecordId": "example-record"
}
```

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 200 | Recorded | Returns the source-attributed measure reading. | AccountMeasureReading |
| 409 | Conflict | A company_total reading was requested on a child Account. | — |

---

## POST /external/v1/accounts/import/salesforce/inspect — Inspect a Salesforce account

Operation ID: `inspect-salesforce-account`

Inspect one exact Salesforce source record through the application import service. The record contains only these seven fields when present: Id, Name, ParentId, Website, LastModifiedDate, PGID__c, and PGID_OID__c. Account-read scope does not expose arbitrary raw CRM properties.

Required API key scopes: `accounts:read`

### JSON request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `integrationId` | string | Yes | Salesforce connection in this workspace. |
| `sourceRecordId` | string | Yes | Exact Salesforce Account source record ID. |

### Example request

```json
{
  "integrationId": "00000000-0000-4000-8000-000000000001",
  "sourceRecordId": "001AbCdEfGhIjKl"
}
```

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 201 | Success | Returns integrationId, sourceRecordId, and the seven-field record projection. No Account is changed. | SalesforceAccountInspection |

---

## POST /external/v1/accounts/import/salesforce/apply — Apply reviewed Salesforce account imports

Operation ID: `apply-salesforce-account-import`

Apply reviewed Salesforce link, create, or skip actions through the same source-backed import service as the application. Every link or create action must explicitly provide selectedFields. An empty list links identity only; omitted selection is rejected. Returned created and updated Accounts use the public Account response shape, not raw CRM records.

Required API key scopes: `accounts:write`

### JSON request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `integrationId` | string | Yes | Salesforce connection in this workspace. |
| `actions` | object[] | Yes | 1–500 {sourceRecordId, action, targetAccountId?, selectedFields} actions. action is link, create, or skip; link requires a target Account. selectedFields accepts up to 20 supported field keys and is required for link/create, including [] for identity only. |

### Example request

```json
{
  "integrationId": "00000000-0000-4000-8000-000000000001",
  "actions": [
    {
      "sourceRecordId": "001AbCdEfGhIjKl",
      "action": "link",
      "targetAccountId": "00000000-0000-4000-8000-000000000002",
      "selectedFields": []
    }
  ]
}
```

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 201 | Applied | Returns created and updated public Accounts with skipped and failed action results. | CrmAccountImportResult |
| 400 | Invalid selection | A link/create action omitted selectedFields or the request shape is invalid. | — |

---

## POST /external/v1/accounts/import/pendo-account-links/correction/preview — Preview a Pendo account correction

Operation ID: `preview-pendo-account-correction`

Preview one exact app-scoped identity, target Salesforce proof, binding, all response attributions, and managed/local Signal links. Caps are 1,000 responses and 250 Signals per identity; exceeding either cap rejects the request instead of returning a partial correction. The API key creator must currently be an owner or admin of this workspace; a missing creator fails closed. Workspace and actor cannot be supplied in the body. The preview returns only receipt IDs, Account/Signal IDs, status and update time, plus Signal public IDs and control summaries. No raw provider response IDs, response content, personal response identities, Account mappings, or processing payloads are returned. Preview checks Signal processing status, not provider queue or Redis availability.

Required API key scopes: `accounts:read`

### JSON request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `integrationId` | uuid | Yes | A Pendo connection in this workspace. |
| `appId` | string | Yes | Exact Pendo app ID, not a product display name. |
| `externalId` | string | Yes | Exact imported app-scoped Account value, including its prefix. |
| `targetAccountId` | uuid | Yes | The reviewed target Account. A fuzzy candidate match is not required. |
| `targetSalesforceSourceKey` | string | Yes | Exact salesforce:<org-id>:account source key that proves the target. |
| `targetSalesforceExternalId` | string | Yes | Salesforce Account ID, 15 or 18 characters. The first 15 characters retain case and must bind uniquely to the target. |
| `reviewReference` | string | Yes | Non-empty review reference, up to 512 characters. Do not include personal or confidential response data. |
| `reviewedLegacyLinks` | object[] | No | Up to 250 explicit {signalId, accountId, action} choices, with action adopt or remove. Unproven human links remain unless individually reviewed; a receipt alone does not prove ownership. |

### Example request

```json
{
  "integrationId": "00000000-0000-4000-8000-000000000001",
  "appId": "example-app",
  "externalId": "cloud9_subdomain:example",
  "targetAccountId": "00000000-0000-4000-8000-000000000002",
  "targetSalesforceSourceKey": "salesforce:example-org:account",
  "targetSalesforceExternalId": "001AbCdEfGhIjKl",
  "reviewReference": "Example workbook/Main/42",
  "reviewedLegacyLinks": []
}
```

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 201 | Preview | Returns identity, target, binding, responseHistory, signals, summary, caps, driftFingerprint, canApply, and blockers. No correction is applied. | PendoAccountCorrectionPreview |
| 400 | Invalid or too large | An exact identity/review is missing or a history cap is exceeded. | — |
| 403 | Forbidden | The key creator is not a current workspace owner/admin. | — |
| 409 | Conflict | The Salesforce target proof or Pendo source scope is not uniquely verified. | — |

---

## POST /external/v1/accounts/import/pendo-account-links/correction/apply — Apply a reviewed Pendo account correction

Operation ID: `apply-pendo-account-correction`

Apply the exact reviewed identity and choices using the current-data driftFingerprint from preview. The service locks and rereads affected records, checks the processing queue, and rejects drift before writing. It journals before/after attribution, reconciles response and managed Signal contributions, preserves unreviewed human links, local exclusions, and other providers, and retains the correction on repeat imports. Generic unrestricted Pendo correction remains blocked. Caps remain 1,000 responses and 250 Signals. The API key creator must currently be an owner or admin of this workspace; a missing creator fails closed. Workspace and actor cannot be supplied in the body. Changed Signals use the existing processing service after commit; queue failure does not undo a committed correction and is reported as pending work. Raw journal snapshots are not returned.

Required API key scopes: `accounts:write`

### JSON request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `integrationId` | uuid | Yes | A Pendo connection in this workspace. |
| `appId` | string | Yes | Exact Pendo app ID, not a product display name. |
| `externalId` | string | Yes | Exact imported app-scoped Account value, including its prefix. |
| `targetAccountId` | uuid | Yes | The reviewed target Account. A fuzzy candidate match is not required. |
| `targetSalesforceSourceKey` | string | Yes | Exact salesforce:<org-id>:account source key that proves the target. |
| `targetSalesforceExternalId` | string | Yes | Salesforce Account ID, 15 or 18 characters. The first 15 characters retain case and must bind uniquely to the target. |
| `reviewReference` | string | Yes | Non-empty review reference, up to 512 characters. Do not include personal or confidential response data. |
| `reviewedLegacyLinks` | object[] | No | Up to 250 explicit {signalId, accountId, action} choices, with action adopt or remove. Unproven human links remain unless individually reviewed; a receipt alone does not prove ownership. |
| `driftFingerprint` | string | Yes | The 64-character lowercase hexadecimal fingerprint from a fresh preview of these exact review choices. |

### Example request

```json
{
  "integrationId": "00000000-0000-4000-8000-000000000001",
  "appId": "example-app",
  "externalId": "cloud9_subdomain:example",
  "targetAccountId": "00000000-0000-4000-8000-000000000002",
  "targetSalesforceSourceKey": "salesforce:example-org:account",
  "targetSalesforceExternalId": "001AbCdEfGhIjKl",
  "reviewReference": "Example workbook/Main/42",
  "reviewedLegacyLinks": [],
  "driftFingerprint": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}
```

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 201 | Applied | Returns correctionId, bindingId, responseCount, signalCount, processingSignalIds, queuedSignalIds, pendingSignalIds, and journalUpdatePending. | PendoAccountCorrectionResult |
| 403 | Forbidden | The key creator is not a current workspace owner/admin. | — |
| 409 | Conflict | History changed, processing is active, or source/control proof blocks correction. Preview again; no correction was saved. | — |

---

## POST /external/v1/accounts/import/pendo-account-links/correction/retry-processing — Retry Pendo correction processing

Operation ID: `retry-pendo-account-correction-processing`

Retry failed queue submissions or failed Signals from this correction's bounded processing journal. Successful Signals and Signals outside the journal are not reprocessed. This does not reapply Account attribution. The API key creator must currently be an owner or admin of this workspace; a missing creator fails closed. Workspace and actor cannot be supplied in the body.

Required API key scopes: `accounts:write`

### JSON request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `correctionId` | uuid | Yes | The committed Pendo correction UUID returned by apply. |

### Example request

```json
{
  "correctionId": "00000000-0000-4000-8000-000000000003"
}
```

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 201 | Retried | Returns correctionId, queuedSignalIds, pendingSignalIds, and journalUpdatePending. | PendoAccountCorrectionProcessingResult |
| 403 | Forbidden | The key creator is no longer a workspace owner/admin. | — |
| 404 | Not found | The correction is not in this workspace. | — |

---

## GET /external/v1/accounts/:id/merge-operations — List account merge operations

Operation ID: `list-account-merge-operations`

List the latest 50 merge operations for the surviving Account, newest first. Returns operation IDs, survivor/loser IDs, actor, source, status, and recovery timestamps. Raw before/after snapshots are never exposed. Uses the same recovery journal service as the application.

Required API key scopes: `accounts:read`

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | uuid | Yes | The Account UUID in the API-key workspace. |

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 200 | Success | Returns public operation metadata. | Array<AccountMergeOperation> |

---

## POST /external/v1/accounts/merge-operations/:operationId/undo-preview — Preview account merge undo

Operation ID: `preview-account-merge-undo`

Review unchanged-only recovery of a journaled merge within its seven-day window. Returns canUndo and blockers, never raw snapshots. Changed data, expired windows, unsupported reference graphs, or unsupported schema changes block automatic undo; there is no force mode. A successful preview does not guarantee a later undo if data changes.

Required API key scopes: `accounts:read`

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `operationId` | uuid | Yes | The merge operation UUID in the API-key workspace. |

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 201 | Preview | Returns operationId, survivorId, loserIds, status, undoExpiresAt, mode, canUndo, and blockers. | AccountMergeUndoPreview |
| 404 | Not found | The merge operation is not in this workspace. | — |

---

## POST /external/v1/accounts/merge-operations/:operationId/undo — Undo an unchanged account merge

Operation ID: `undo-account-merge`

Restore a supported journaled merge atomically, only within seven days and only if affected data and references remain unchanged. Both accounts:write AND accounts:delete are required. The service locks and checks current data again; unsupported graphs and drift block automatic undo. No force mode, partial recovery, or raw snapshot response is available.

Required API key scopes: `accounts:write`, `accounts:delete`

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `operationId` | uuid | Yes | The merge operation UUID in the API-key workspace. |

### JSON request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `mode` | string | Yes | Must be unchanged-only. |

### Example request

```json
{
  "mode": "unchanged-only"
}
```

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 201 | Restored | Returns operationId, status undone, survivorId, and restoredAccountIds. | AccountMergeUndoResult |
| 409 | Blocked | Recovery is expired, already used, changed, unsupported, or busy. No recovery changes were saved. | — |

---

## GET /external/v1/accounts/:id/contacts — List account contacts

Operation ID: `list-account-contacts`

List contacts linked to an account in the current workspace. Use this before contact sync automation rather than relying on account `contactIds` alone.

Required API key scopes: `accounts:read`

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | uuid | Yes | The backend account UUID. |

### Example request

```curl
curl -X GET https://zentrik.ai/api/external/v1/accounts/account-uuid/contacts \
  -H 'Authorization: Bearer YOUR_API_KEY'
```

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 200 | Success | Contacts were successfully retrieved. | Array<Contact> |
| 404 | Not Found | No account found with the provided id in the current workspace. | — |

### Example response

```json
[
  {
    "id": "contact-uuid",
    "accountId": "account-uuid",
    "name": "Primary product contact",
    "role": "VP Product",
    "email": "product-contact@example.com",
    "phone": null,
    "notes": "Primary product evaluator and champion.",
    "isPrimary": true,
    "createdAt": "2026-04-09T10:00:00Z",
    "updatedAt": "2026-04-09T10:00:00Z"
  }
]
```

---

## POST /external/v1/accounts/:id/contacts — Create an account contact

Operation ID: `create-account-contact`

Create one contact on an account. This is the preferred way to add contacts; do not patch the account `contacts` array for incremental updates.

Required API key scopes: `accounts:write`

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | uuid | Yes | The backend account UUID. |

### JSON request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | Yes | Contact display name. |
| `role` | string | No | Role, title, or account-specific responsibility. |
| `email` | string | No | Email address. |
| `phone` | string | No | Phone number. |
| `notes` | string | No | Operator notes such as influence, interests, and outreach guidance. |
| `isPrimary` | boolean | No | Whether this is the primary contact for the account. |

### Example request

```json
{
  "name": "Primary product contact",
  "role": "VP Product",
  "email": "product-contact@example.com",
  "notes": "Primary product evaluator and champion.",
  "isPrimary": true
}
```

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 201 | Created | The contact was successfully created. | Contact |
| 404 | Not Found | No account found with the provided id in the current workspace. | — |

### Example response

```json
{
  "id": "contact-uuid",
  "accountId": "account-uuid",
  "name": "Primary product contact",
  "role": "VP Product",
  "email": "product-contact@example.com",
  "notes": "Primary product evaluator and champion.",
  "isPrimary": true,
  "createdAt": "2026-04-09T10:00:00Z",
  "updatedAt": "2026-04-09T10:00:00Z"
}
```

---

## PATCH /external/v1/accounts/:id/contacts/:contactId — Update an account contact

Operation ID: `update-account-contact`

Update one contact on an account. The contact must belong to the requested account in the current workspace.

Required API key scopes: `accounts:write`

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | uuid | Yes | The backend account UUID. |
| `contactId` | uuid | Yes | The backend contact UUID. |

### JSON request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | No | Updated contact display name. |
| `role` | string | No | Updated role, title, or account-specific responsibility. |
| `email` | string | No | Updated email address. |
| `phone` | string | No | Updated phone number. |
| `notes` | string | No | Updated operator notes. |
| `isPrimary` | boolean | No | Updated primary-contact flag. |

### Example request

```curl
curl -X PATCH https://zentrik.ai/api/external/v1/accounts/account-uuid/contacts/contact-uuid \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"notes":"Strategic evaluator. Reach out for roadmap and pricing context."}'
```

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 200 | Updated | The contact was successfully updated. | Contact |
| 404 | Not Found | No account/contact pair found in the current workspace. | — |

---

## DELETE /external/v1/accounts/:id/contacts/:contactId — Delete an account contact

Operation ID: `delete-account-contact`

Remove one person from an Account without deleting the person or their engagement history.

Required API key scopes: `accounts:write`

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | uuid | Yes | The backend account UUID. |
| `contactId` | uuid | Yes | The backend contact UUID. |

### Example request

```curl
curl -X DELETE https://zentrik.ai/api/external/v1/accounts/account-uuid/contacts/contact-uuid \
  -H 'Authorization: Bearer YOUR_API_KEY'
```

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 200 | Unassigned | The person is now unassigned and remains available in People. | — |
| 404 | Not Found | No account/contact pair found in the current workspace. | — |
