---
title: "Research API reference"
canonical_url: https://zentrik.ai/docs/api/research
markdown_url: https://zentrik.ai/docs/api/research.md
last_reviewed: 2026-10-19
---

# Research API reference

Use saved competitor, market, and public-feedback research with workspace API keys. Read results, review a plan, start research, add saved evidence to Discovery, then choose whether to keep it updated. Read access, permission to run research, and permission to manage updates are separate scopes.

- Human reference: https://zentrik.ai/docs/api/research
- 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/research — Find saved research

Operation ID: `list-research`

Find saved research by name, question, subject, or scope. Returns brief identities, states, row versions, cadence, and next-run times. The body contains items and pagination with total, limit, offset, and hasMore. No model or source-provider calls occur. A failed read is an error, not an empty library.

Required API key scopes: `research:read`

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `q` | string | No | Text search, at most 120 characters. |
| `limit` | integer | No | Page size, 1–50. Default 10. |
| `offset` | integer | No | Matching items to skip. Default 0. For run history, use cursor instead. |

### Example request

```curl
curl 'https://zentrik.ai/api/external/v1/research?q=planning&limit=10' -H 'Authorization: Bearer YOUR_API_KEY'
```

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 200 | Success | Saved data or the requested action receipt. | — |
| 400 | Invalid request | Invalid identity, unsupported fields, incomplete command, or invalid plan. | — |
| 403 | Access required | Missing API scope or current workspace role for a write. | — |
| 404 | Not found | The research or run does not belong to this workspace. | — |
| 409 | Review required | Stale plan, incompatible run, unavailable destination, or a schedule that cannot be changed. | — |

### Example response

```json
{
  "items": [
    {
      "id": "11111111-1111-4111-8111-111111111111",
      "name": "Planning tools",
      "state": "draft",
      "rowVersion": 1
    }
  ],
  "pagination": {
    "total": 1,
    "limit": 10,
    "offset": 0,
    "hasMore": false
  }
}
```

---

## GET /external/v1/research/:id — Inspect saved research

Operation ID: `get-research`

Read the editable plan, run progress, dated history, a section’s answer and cited web findings, or original directly read reviews. Use saved run and section IDs for comparisons over time. Compare the original questions, scope, and recorded date windows from view=run rather than the current edited plan. Reads do not start research. brief returns {brief}; progress returns {run}, null when no run exists; run requires runId and returns {run, definition, fromDateTime, toDateTime, reviewThemes, scores} from that saved execution, with null dates if not recorded yet; reviewThemes counts what each app’s new reviews raise, and scores gives each confirmed app’s published store rating with, when measurable, how many ratings arrived since the previous run of the same plan and their mean; runs returns a keyset page with items, hasMore, and nextCursor; findings returns answer, items, savedEvidence source snapshots, and pagination; each answered question’s comparison.status (changed, consistent, or insufficient) states what changed since answer.previousRunId, which is null on a first run; records returns items and pagination. Findings keep their source URL, observations, dates, relevance, and provenance. Unknown dates remain unknown. Records are the directly read source items; web sources are in findings. Run receipts omit full snapshots and answers. Back off polling when progress is unchanged.

Required API key scopes: `research:read`

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | UUID | Yes | Research brief identity from the list (path). |
| `view` | enum | No | brief (default), progress, runs, run, findings, or records. |
| `runId` | UUID | No | Required for run, findings, or records. Must belong to this brief. |
| `sectionId` | UUID | No | Required for findings. Use a section identity from the plan or run receipt. |
| `cursor` | string | No | The prior nextCursor for view=runs. At most 1,000 characters. |
| `limit` | integer | No | Page size, 1–50. Default 10. |
| `offset` | integer | No | Matching items to skip. Default 0. For run history, use cursor instead. |

### Example request

```curl
curl 'https://zentrik.ai/api/external/v1/research/11111111-1111-4111-8111-111111111111?view=brief' -H 'Authorization: Bearer YOUR_API_KEY'
```

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 200 | Success | Saved data or the requested action receipt. | — |
| 400 | Invalid request | Invalid identity, unsupported fields, incomplete command, or invalid plan. | — |
| 403 | Access required | Missing API scope or current workspace role for a write. | — |
| 404 | Not found | The research or run does not belong to this workspace. | — |
| 409 | Review required | Stale plan, incompatible run, unavailable destination, or a schedule that cannot be changed. | — |

### Example response

```json
{
  "brief": {
    "id": "11111111-1111-4111-8111-111111111111",
    "rowVersion": 1,
    "configuration": {
      "state": "draft",
      "draft": {
        "revision": 1,
        "definition": {
          "researchMode": "baseline",
          "maxSourcesPerSection": 3,
          "sections": [
            {
              "id": "22222222-2222-4222-8222-222222222222",
              "type": "market",
              "name": "Planning tools",
              "topic": "Product planning tools",
              "objective": "Understand unmet needs",
              "questions": [
                "Which needs remain unmet?"
              ]
            }
          ]
        }
      }
    }
  }
}
```

---

## POST /external/v1/research/run — Prepare, run, or deliver research

Operation ID: `run-research`

Choose one action in command.action. prepare drafts a plan from question (8–3,000 characters); review it before start. If apps are ambiguous, continue prepare with preparedPlan from the response and selection [{ connector: "app_store", externalId, label }] from its choices, at most four. start requires requestId (UUID), name (1–120 characters), and definition; editing also requires briefId and expectedRowVersion. Reuse requestId for HTTP retries of the same start request; use a new UUID for an intentional fresh run. start returns briefId and run. add requires briefId, runId, and productId and queues the exact saved sources without a new search. The destination cannot change after addition. retry, retry_addition, and stop require briefId and runId. Retry retains original dates and reusable completed work; stop preserves sealed preview results. Preparation, search, and evidence processing use AI credits; no per-run estimate is available. A run receipt confirms queued or completed work; poll progress and read the saved findings before reporting success or adding evidence. The API key’s creator must still be an active member with edit access. Keys with no creator can read but cannot run research. comparison_plan takes productId, 1–4 remembered competitorIds, optional lenses (live, selling, buyers, customers, enterprise, momentum; the first four by default), an optional own question (8–500 characters) and acquisition (web by default, or saved_only). Each lens is one question; more than five questions is rejected. It returns the competitive landscape plan the app uses, without model or provider calls, and landscape { briefId, expectedRowVersion } for the product’s existing landscape, or null. Review the plan, then start it with those values so each answer compares with the previous run; answer.previousRunId names that run, so check its dates before calling a change weekly.

Required API key scopes: `research:run`

### JSON request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `command` | object | Yes | Exactly one comparison_plan, prepare, start, add, retry, retry_addition, or stop command as described above. Unsupported fields are rejected. |
| `command.definition` | object | No | Required for start. Use a reviewed prepare response or saved draft definition. Only the fields below are accepted. Preparation defaults to three sources per section; a directly supplied definition defaults to five. Set maxSourcesPerSection explicitly to control the target. |
| `command.definition.sections` | object[] | No | 1–5 sections. Each needs a distinct UUID id, name (1–120 characters), type (competitors, market, or custom), objective (1–1,500 characters), and 1–5 questions (1–500 characters each). competitors sections need 1–5 competitors with name (1–160), optional website, and at most five aliases. market sections need topic (1–500); custom sections can have an optional topic (at most 500). |
| `command.definition.sections[].savedSignalIds` | UUID[] | No | Up to five distinct workspace Signal IDs. Sources are captured with their original revision when the run reads them; no new Signals are created for these inputs. |
| `command.definition.sections[].includeProductContext` | boolean | No | Include this product’s internal description as stated intent. Requires productId. This is not proof of shipped capabilities. |
| `command.definition.sections[].acquisition` | enum | No | web (default) combines selected saved sources with public research; saved_only answers only from saved inputs. Competitor identity snapshots may carry a remembered competitorId. |
| `command.definition.sections[].scope` | object | No | Optional geography (at most 160 characters), audience (300), exclusions (1,000), languages (1–5 labels, 1–50 each; default English), sourceMode (open, preferred, restricted; default open), domains and excludedDomains (at most 20 each). Preferred and restricted require at least one domain. Excluded domains remain blocked. |
| `command.definition.researchMode` | enum | No | recent (default) requires qualifying dates; baseline can retain relevant older or undated sources. |
| `command.definition.initialLookbackDays` | integer | No | 1–90, default 30. Retries preserve their original dates. |
| `command.definition.maxSourcesPerSection` | integer | No | 1–10. Qualifying source target, not guaranteed coverage. First previews use one search round per section. |
| `command.definition.outputLanguage` | string | No | 1–50 characters, default English. |
| `command.definition.productId` | UUID \| empty string | No | Plan destination; required as the baseline identity when includeProductContext is true. Such comparisons can only be added to that product. Otherwise the explicit productId in add controls delivery. |
| `command.definition.reads` | object[] | No | At most four confirmed App Store selections: connector="app_store", numeric externalId (1–20 digits), label (1–200 characters), and optional sectionIds from this plan. No duplicate apps. Copy the reviewed planner selections; do not invent app identities. |

### Example request

```json
{
  "command": {
    "action": "start",
    "requestId": "33333333-3333-4333-8333-333333333333",
    "name": "Planning tools",
    "definition": {
      "researchMode": "baseline",
      "maxSourcesPerSection": 3,
      "sections": [
        {
          "id": "22222222-2222-4222-8222-222222222222",
          "type": "market",
          "name": "Planning tools",
          "topic": "Product planning tools",
          "objective": "Understand unmet needs",
          "questions": [
            "Which needs remain unmet?"
          ]
        }
      ]
    }
  }
}
```

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 200 | Success | Saved data or the requested action receipt. | — |
| 400 | Invalid request | Invalid identity, unsupported fields, incomplete command, or invalid plan. | — |
| 403 | Access required | Missing API scope or current workspace role for a write. | — |
| 404 | Not found | The research or run does not belong to this workspace. | — |
| 409 | Review required | Stale plan, incompatible run, unavailable destination, or a schedule that cannot be changed. | — |

### Example response

```json
{
  "briefId": "11111111-1111-4111-8111-111111111111",
  "run": {
    "id": "44444444-4444-4444-8444-444444444444",
    "operationId": "11111111-1111-4111-8111-111111111111",
    "status": "queued",
    "sections": [],
    "nativeReadCount": 0
  }
}
```

---

## POST /external/v1/research/updates — Manage research updates

Operation ID: `update-research`

save_context takes productId and the complete context { version, website, baselineSignalIds, competitors }. website is your product’s public site: with it, a landscape reads your public pages as it reads competitors’; without it, your column answers from product context and baseline Signals only. Each competitor has a stable UUID id, name, optional website, aliases (up to five), notes (up to 1,000 characters), and signalIds (up to five). Keep at most 30 competitors per product. Read the context first. Give a new competitor a new UUID; an entry left out is removed. A stale version returns 409; reload and reconcile before retrying. Editors can save this memory. Other actions require briefId and owner or admin access. keep also requires the runId returned by a successful add and an explicit runPolicy (on_demand, daily, or weekly); it activates only the current reviewed plan after adding its findings. schedule changes an active brief using runPolicy and optional timezone. keep also accepts timezone. pause stops future automatic runs; resume restores the saved cadence; archive requires a paused brief and preserves history and Signals. Daily and weekly research consume credits when runs execute and add new sources to Discovery. Nothing notifies users of changes; read findings to report them. Schedules managed by Zentrik and workspace plan limits remain enforced. These settings are separate from Brief emails. The API key’s creator must still be an active workspace owner or admin; a run scope alone cannot manage schedules.

Required API key scopes: `research:manage`

### JSON request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `command` | object | Yes | Exactly one save_context, keep, schedule, pause, resume, or archive command as described above. Unsupported fields are rejected. |

### Example request

```json
{
  "command": {
    "action": "pause",
    "briefId": "11111111-1111-4111-8111-111111111111"
  }
}
```

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 200 | Success | Saved data or the requested action receipt. | — |
| 400 | Invalid request | Invalid identity, unsupported fields, incomplete command, or invalid plan. | — |
| 403 | Access required | Missing API scope or current workspace role for a write. | — |
| 404 | Not found | The research or run does not belong to this workspace. | — |
| 409 | Review required | Stale plan, incompatible run, unavailable destination, or a schedule that cannot be changed. | — |

### Example response

```json
{
  "brief": {
    "id": "11111111-1111-4111-8111-111111111111",
    "configuration": {
      "state": "paused"
    }
  }
}
```

---

## GET /external/v1/research/products/:productId — Read product competitors and saved sources

Operation ID: `get-research-context`

Read a product’s shared competitor memory, internal baseline context and linked Signal identities with view=context (default). view=sources returns paginated workspace Signals, independent of ingestion channel, with extracted research subjects when available. Use q to search names and source metadata. The context view also returns the landscape lenses and suggestions: alternatives that saved research reports evaluated and the product does not remember yet. No model or provider calls occur. A report subject or suggestion is a candidate identity, not proof of a competitive relationship. Saving a context or creating a comparison requires a separate command.

Required API key scopes: `research:read`

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `productId` | UUID | Yes | Product in this workspace (path). |
| `view` | enum | No | context (default) or sources. |
| `q` | string | No | Source search, at most 120 characters. |
| `limit` | integer | No | Sources page size, 1–50. Default 10. |
| `offset` | integer | No | Sources to skip. Default 0. |

### Example request

```curl
curl 'https://zentrik.ai/api/external/v1/research/products/66666666-6666-4666-8666-666666666666' -H 'Authorization: Bearer YOUR_API_KEY'
```

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 200 | Success | Saved data or the requested action receipt. | — |
| 400 | Invalid request | Invalid identity, unsupported fields, incomplete command, or invalid plan. | — |
| 403 | Access required | Missing API scope or current workspace role for a write. | — |
| 404 | Not found | The research or run does not belong to this workspace. | — |
| 409 | Review required | Stale plan, incompatible run, unavailable destination, or a schedule that cannot be changed. | — |

### Example response

```json
{
  "product": {
    "id": "66666666-6666-4666-8666-666666666666",
    "name": "Atlas",
    "description": "Product team context"
  },
  "context": {
    "version": 0,
    "website": "",
    "baselineSignalIds": [],
    "competitors": []
  },
  "lenses": [
    {
      "key": "selling",
      "label": "How they sell",
      "hint": "Pricing, plans, trials and the path to buy",
      "question": "How is it sold: pricing, plans and packaging, trial or free tier, and whether buyers self-serve or go through sales?",
      "labels": [
        "Entry price",
        "Billing unit",
        "Free tier",
        "Trial",
        "Buying route",
        "Enterprise plan"
      ],
      "default": true
    }
  ],
  "suggestions": [
    {
      "name": "Beacon",
      "website": "https://beacon.example/",
      "signalIds": [],
      "count": 2
    }
  ],
  "sources": []
}
```
