> ## Documentation Index
> Fetch the complete documentation index at: https://docs.coverbase.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Risk API

> Read and edit the risk register, run and save scenarios, read exposure and the board report, and manage risk domains.

For AI agents: a documentation index is available at [https://docs.coverbase.com/llms.txt](https://docs.coverbase.com/llms.txt). This page is also available in markdown by appending .md to the URL.

The Risk API gives you the same risk register, scenarios and reports the Risk page shows. A **risk** is an entry on the register with an inherent and a residual score, an owner, a scope and the signals (findings, contract gaps, alerts and other records) that support it.

Every endpoint is scoped to the organization of the API key. See [API conventions](/conventions.md) for shared behavior.

| Method | Path | Permission |
| - | - | - |
| `POST` | `/v1/risks/query` | `risk:read:org` |
| `GET` | `/v1/risks/summary` | `risk:read:org` |
| `GET` | `/v1/risks/matrix` | `risk:read:org` |
| `POST` | `/v1/risks` | `risk:create:org` |
| `GET` | `/v1/risks/{risk_id}` | `risk:read:org` |
| `PATCH` | `/v1/risks/{risk_id}` | `risk:update:org` |
| `POST` | `/v1/risks/{risk_id}/signals` | `risk:update:org` |
| `POST` | `/v1/risks/{risk_id}/signals/{signal_id}/dismiss` | `risk:update:org` |
| `GET` | `/v1/risk-exposure` | `risk:read:org` |
| `GET` | `/v1/risk-report` | `risk:read:org` |
| `GET` | `/v1/risk-config` | `risk:read:org` |
| `POST` | `/v1/risk-scenarios/run` | `risk:run:org` |
| `GET` | `/v1/risk-scenarios` | `risk:read:org` |
| `POST` | `/v1/risk-scenarios` | `risk:create:org` |
| `GET` | `/v1/risk-scenarios/{scenario_id}` | `risk:read:org` |
| `GET` | `/v1/risk-domains` | `risk:read:org` |
| `POST` | `/v1/risk-domains` | `risk:update:org` |
| `GET` | `/v1/risk-domains/{risk_domain_id}` | `risk:read:org` |
| `PATCH` | `/v1/risk-domains/{risk_domain_id}` | `risk:update:org` |
| `GET` | `/v1/vendors/{vendor_id}/risk-score-history` | `vendor:read:org` |
| `GET` | `/v1/risk-methodology` | `org_settings:read:org` |
| `GET` | `/v1/reviewer-matrix-rules` | `org_settings:read:org` |
| `GET` | `/v1/work-queue-escalation-rules` | `org_settings:read:org` |
| `GET` | `/v1/ai-settings` | `org_settings:read:org` |
| `GET` | `/v1/agent-actions` | `org_settings:read:org` |
| `GET` | `/v1/vendors/{vendor_id}/access-removal-requests` | `integration:read:org` |
| `GET` | `/v1/access-removal-requests/{request_id}` | `integration:read:org` |
| `PATCH` | `/v1/risk-config` | `org_settings:update:org` |
| `POST` | `/v1/risks/{risk_id}/commitment-requests` | `risk:update:org` |
| `POST` | `/v1/risk-chain/query` | `risk:read:org` |
| `PUT` | `/v1/risk-methodology` | `org_settings:update:org` |
| `POST` | `/v1/reviewer-matrix-rules` | `org_settings:update:org` |
| `PUT` | `/v1/reviewer-matrix-rules/{rule_id}` | `org_settings:update:org` |
| `POST` | `/v1/work-queue-escalation-rules` | `org_settings:update:org` |
| `PUT` | `/v1/work-queue-escalation-rules/{rule_id}` | `org_settings:update:org` |
| `PATCH` | `/v1/ai-settings` | `org_settings:update:org` |
| `GET` | `/v1/ai-settings/learned-examples` | `org_settings:read:org` |
| `GET` | `/v1/vendors/{vendor_id}/due-diligence-status` | `vendor:read:org` |
| `GET` | `/v1/findings-stats` | `finding:read:org` |
| `POST` | `/v1/commitments/{commitment_id}/extend-due-date` | `portal:update:org` and `commitment:update:org` |
| `GET` | `/v1/questionnaire-versions` | `questionnaire:read:org` |
| `POST` | `/v1/questionnaires/{questionnaire_id}/draft-version` | `questionnaire:update:org` |
| `POST` | `/v1/questionnaires/{questionnaire_id}/publish-version` | `questionnaire:update:org` |
| `GET` | `/v1/responsibility-topics` | `vendor:read:org` |
| `POST` | `/v1/responsibility-topics` | `org_settings:update:org` |
| `GET` | `/v1/vendors/{vendor_id}/responsibility-matrix` | `vendor:read:org` |
| `POST` | `/v1/vendors/{vendor_id}/responsibility-matrix` | `vendor:update:org` |
| `GET` | `/v1/questionnaires/{questionnaire_id}/responses/{response_id}/attestation` | `questionnaire:read:org` |
| `GET` | `/v1/assessments/{assessment_id}/review-sessions` | `assessment:read:org` |
| `GET` | `/v1/work-queue-items` | `assessment:read:org` |
| `GET` | `/v1/work-queue-items/{work_queue_item_id}` | `assessment:read:org` |
| `POST` | `/v1/work-queue-items/{work_queue_item_id}/complete` | `assessment:update:org` |
| `POST` | `/v1/work-queue-items/{work_queue_item_id}/reopen` | `assessment:update:org` |
| `POST` | `/v1/work-queue-items` | `assessment:update:org` |
| `PATCH` | `/v1/work-queue-items/{work_queue_item_id}` | `assessment:update:org` |
| `POST` | `/v1/work-queue-items/reassign` | `assessment:update:org` |
| `GET` | `/v1/finding-acceptance-rules` | `finding:read:org` |
| `POST` | `/v1/finding-acceptance-rules` | `org_settings:update:org` |
| `PUT` | `/v1/finding-acceptance-rules/order` | `org_settings:update:org` |
| `PUT` | `/v1/finding-acceptance-rules/{rule_id}` | `org_settings:update:org` |
| `GET` | `/v1/findings/{finding_id}/decision-path` | `finding:read:org` |
| `GET` | `/v1/risks/{risk_id}/signal-suggestions` | `risk:read:org` |
| `GET` | `/v1/risk-chain/nodes/{node_id}` | `risk:read:org` |
| `POST` | `/v1/risk-chain/rebuild` | `risk:run:org` |
| `POST` | `/v1/risk-chain/lineage` | `risk:read:org` |
| `POST` | `/v1/risk-geography/query` | `risk:read:org` and `vendor:read:org` |
| `GET` | `/v1/risk-scenarios/subjects` | `risk:read:org` |
| `POST` | `/v1/risk-scenarios/{scenario_id}/attach` | `risk:update:org` |
| `POST` | `/v1/risk-scenarios/create-risks` | `risk:create:org` |
| `POST` | `/v1/risk-ask` | `risk:run:org` |
| `POST` | `/v1/risk-agent/run` | `risk:run:org` |
| `GET` | `/v1/risk-narrative` | `risk:read:org` |
| `PUT` | `/v1/risk-narrative` | `risk:update:org` |
| `PUT` | `/v1/risk-narrative/instructions` | `risk:update:org` |
| `GET` | `/v1/vendor-tier-rule` | `org_settings:read:org` |
| `POST` | `/v1/vendor-tier-rule/preview` | `org_settings:update:org` |
| `PUT` | `/v1/vendor-tier-rule` | `org_settings:update:org` |
| `GET` | `/v1/risk-methodology/scale-normalization` | `org_settings:read:org` |
| `POST` | `/v1/reviewer-matrix-rules/reassign` | `org_settings:update:org` |
| `GET` | `/v1/work-queue-escalation-log` | `org_settings:read:org` |
| `GET` | `/v1/work-queue-items/{work_queue_item_id}/routing-events` | `org_settings:read:org` |
| `GET` | `/v1/agent-actions/{action_id}` | `org_settings:read:org` |
| `GET` | `/v1/agent-decision-rates` | `org_settings:read:org` |
| `PATCH` | `/v1/ai-settings/learned-examples/{example_id}` | `org_settings:update:org` |
| `POST` | `/v1/ai-settings/learned-examples/harvest` | `org_settings:update:org` |
| `GET` | `/v1/risk-domain-control-links` | `risk:read:org` |
| `PUT` | `/v1/risk-domains/{risk_domain_id}/control-links` | `control_set:update:org` |
| `GET` | `/v1/vendors/{vendor_id}/residual-risk-floor` | `vendor:read:org` |
| `GET` | `/v1/services/{service_id}/residual-risk-floor` | `service:read:org` |
| `POST` | `/v1/questionnaire-responses/{response_id}/irq-what-if` | `questionnaire:read:org` |
| `POST` | `/v1/assessments/{assessment_id}/residual-what-if` | `assessment:read:org` |
| `GET` | `/v1/questionnaires/{questionnaire_id}/change-log` | `questionnaire:read:org` |
| `GET` | `/v1/questionnaires/{questionnaire_id}/version-diff` | `questionnaire:read:org` |
| `PUT` | `/v1/questionnaires/{questionnaire_id}/content-review` | `questionnaire:update:org` |
| `POST` | `/v1/questionnaires/{questionnaire_id}/content-review` | `questionnaire:update:org` |
| `PUT` | `/v1/questionnaires/{questionnaire_id}/attestation-config` | `questionnaire:update:org` |
| `PATCH` | `/v1/responsibility-topics/{topic_id}` | `org_settings:update:org` |
| `PUT` | `/v1/responsibility-topics/order` | `org_settings:update:org` |
| `GET` | `/v1/vendors/{vendor_id}/responsibility-matrix/versions/{version_id}` | `vendor:read:org` |
| `POST` | `/v1/vendors/{vendor_id}/responsibility-matrix/generate` | `vendor:update:org` |
| `POST` | `/v1/vendors/{vendor_id}/responsibility-matrix/versions/{version_id}/publish` | `vendor:update:org` |
| `POST` | `/v1/vendors/{vendor_id}/responsibility-matrix/versions/{version_id}/discard` | `vendor:update:org` |
| `POST` | `/v1/vendors/{vendor_id}/responsibility-matrix/revert` | `vendor:update:org` |
| `PUT` | `/v1/vendors/{vendor_id}/engagements/{engagement_id}/service-model` | `vendor:update:org` |
| `GET` | `/v1/vendors/{vendor_id}/engagements/{engagement_id}/responsibility-acknowledgements` | `vendor:read:org` |
| `GET` | `/v1/vendors/{vendor_id}/engagements/{engagement_id}/unlinked-cuecs` | `vendor:read:org` |
| `GET` | `/v1/vendors/{vendor_id}/engagements/{engagement_id}/rights` | `vendor:read:org` |
| `POST` | `/v1/vendors/{vendor_id}/engagements/{engagement_id}/rights/scope-to-contracts` | `assessment:run:org` |
| `PUT` | `/v1/assessments/{assessment_id}/method` | `assessment:update:org` |
| `POST` | `/v1/work-queue-items/complete` | `assessment:update:org` |
| `GET` | `/v1/findings/{finding_id}/related-findings` | `finding:read:org` |
| `GET` | `/v1/findings/{finding_id}/jira` | `finding:read:org` |
| `GET` | `/v1/findings/{finding_id}/jira/draft` | `finding:update:org` or `finding:update:assigned` |
| `POST` | `/v1/findings/{finding_id}/jira` | `finding:update:org` or `finding:update:assigned` |
| `POST` | `/v1/commitments/{commitment_id}/regenerate-review` | `portal:update:org` and `commitment:update:org` |
| `POST` | `/v1/obligations/bulk-action` | `obligation:update:org`, and `obligation:archive:org` for `delete` |
| `POST` | `/v1/obligations/extract-from-documents` | `obligation:create:org` |
| `POST` | `/v1/assessments/{assessment_id}/review-sessions` | `assessment:update:org` |
| `PUT` | `/v1/assessments/{assessment_id}/review-sessions/{review_session_id}` | `assessment:update:org` |
| `POST` | `/v1/vendors/{vendor_id}/engagements/{engagement_id}/responsibility-acknowledgements` | `vendor:update:org` and `portal:update:org` |
| `GET` | `/v1/questionnaires/{questionnaire_id}/responses/{response_id}/attestation/documents/{org_document_id}` | `questionnaire:read:org` |
| `POST` | `/v1/documents/{document_id}/finding-extraction` | `finding:create:org` |
| `GET` | `/v1/documents/{document_id}/finding-extraction` | `finding:read:org` |
| `POST` | `/v1/documents/{document_id}/finding-extraction/accept` | `finding:create:org` |
| `POST` | `/v1/findings/exports` | `finding:export:org` |
| `GET` | `/v1/findings/exports/{export_id}` | `finding:export:org` |
| `POST` | `/v1/risk-chain/lineage/exports` | `risk:export:org` |
| `GET` | `/v1/risk-chain/lineage/exports/{export_id}` | `risk:export:org` |
| `POST` | `/v1/agent-actions/exports` | `org_settings:read:org` |
| `GET` | `/v1/agent-actions/exports/{export_id}` | `org_settings:read:org` |
| `POST` | `/v1/responsibility-topics/import` | `org_settings:update:org` |
| `POST` | `/v1/risk-narrative/jobs` | `risk:run:org` |
| `POST` | `/v1/finding-suggestions` | `finding:create:org` |
| `POST` | `/v1/vendor-finding-suggestions` | `finding:create:org` |
| `POST` | `/v1/commitment-description-suggestions` | `commitment:create:org` |
| `GET` | `/v1/text-jobs/{job_id}` | The permission that started the job |
| `GET` | `/v1/services/{service_id}/risk-score-history` | `service:read:org` |
| `GET` | `/v1/engagements/{engagement_id}/risk-score-history` | `vendor:read:org` |
| `GET` | `/v1/assessments/{assessment_id}/risk-score-history` | `assessment:read:org` |
| `POST` | `/v1/agent-actions/{action_id}/decision` | `org_settings:update:org`; needs a user key |
| `POST` | `/v1/agent-actions/decisions` | `org_settings:update:org`; needs a user key |
| `POST` | `/v1/vendors/{vendor_id}/findings-portal` | `commitment:update:org` |
| `POST` | `/v1/commitments/query` | `commitment:read:org`, or `commitment:read:assigned` for your own |
| `POST` | `/v1/commitments/send-to-vendor` | `commitment:update:org` |
| `POST` | `/v1/commitments/send-obligations-to-vendor` | `commitment:create:org` |
| `PATCH` | `/v1/commitments/{commitment_id}` | `commitment:update:org`, or your own commitment with `commitment:update:assigned`; archiving needs `commitment:archive:org` and a user key |
| `POST` | `/v1/commitments/{commitment_id}/resolve-round` | `commitment:update:org`; `commitment:approve:org` to accept; `portal:update:org` to send back or on a portal commitment; deciding needs a user key |
| `GET` | `/v1/commitments/{commitment_id}/documents` | `commitment:read:org` |
| `POST` | `/v1/commitments/{commitment_id}/documents` | `commitment:update:org` |
| `PATCH` | `/v1/commitments/{commitment_id}/documents/{document_id}` | `commitment:update:org` |
| `POST` | `/v1/commitments/{commitment_id}/documents/{document_id}/archive` | `commitment:update:org`; needs a user key |
| `GET` | `/v1/commitments/{commitment_id}/documents/{document_id}/download` | `commitment:read:org` |
| `GET` | `/v1/findings/{finding_id}/documents` | `finding:read:org` |
| `POST` | `/v1/findings/{finding_id}/documents` | `finding:update:org` |
| `PATCH` | `/v1/findings/{finding_id}/documents/{document_id}` | `finding:update:org` |
| `POST` | `/v1/findings/{finding_id}/documents/{document_id}/archive` | `finding:update:org`; needs a user key |
| `GET` | `/v1/findings/{finding_id}/documents/{document_id}/download` | `finding:read:org` |
| `POST` | `/v1/findings/bulk` | `finding:create:org` |

The `/v1/risks`, `/v1/risk-exposure`, `/v1/risk-report`, `/v1/risk-config` and `/v1/risk-scenarios` endpoints need the Risk module. When your organization does not have it, they return `404` with the code `not_enabled`.

Endpoints that need `org_settings:read:org` or `org_settings:update:org` work with a user API key. A service account key does not hold `org_settings`, so it receives `403` from them in an organization that enforces roles.

Archiving a risk or a scenario, and deciding on a risk the Coverbase agent proposed, stay decisions a signed-in user makes in the app. The API does not offer them.

## Errors

Errors return a `detail` object with a `code` and a `message`.

| Status | Code | When |
| - | - | - |
| `403` | `api_key_not_allowed` | The key's role does not hold the permission the endpoint needs. A signed-in user without it gets `403` with no code. |
| `404` | `not_enabled` | The Risk module is not enabled for your organization. |
| `404` | `risk_not_found` | No risk has that ID. |
| `404` | `signal_not_found` | The risk has no signal with that ID. |
| `404` | `scenario_not_found` | No saved scenario has that ID. |
| `404` | `risk_domain_not_found` | No risk domain has that ID. |
| `404` | `job_not_found` | No narrative or suggestion job you started has that ID. |
| `422` | `invalid_risk` | The risk is missing a description or a score is off the scale. |
| `400` | `invalid_risk_domain` | The risk domain change is not valid, for example a duplicate name. |
| `404` | `commitment_not_found` | No commitment has that ID. |
| `404` | `finding_not_found` | No finding has that ID. |
| `404` | `document_not_found` | The finding or commitment has no document with that ID. |
| `400` | `round_action_not_available` | The commitment's round does not offer that action, or no round awaits you. |
| `400` | `commitment_not_editable` | The commitment cannot take that edit, for example an archived commitment. |
| `400` | `commitment_has_no_finding` | The commitment belongs to an obligation, so it takes no documents here. |
| `422` | `invalid_upload` | The uploaded file is empty or too large. |
| `400` | `invalid_finding_source` | The bulk create's source does not exist or does not fit the rows. |

## Query the register

**method** `POST`

`POST /v1/risks/query`

Returns one page of register risks that match the filters. Send an empty body to list every risk except closed ones.

**statuses** `string[]`

Any of `proposed`, `open`, `mitigating`, `accepted` and `closed`.

**risk\_domain\_ids** `string[]`

Risk domains to include.

**vendor\_id** `string`

Only risks scoped to this vendor.

**search** `string`

Text to match in the risk description.

**limit** `integer` (default: 25)

Page size. The API caps it at 200.

**offset** `integer` (default: 0)

Rows to skip.

The body also accepts `categories`, `owner_user_ids`, `owner_department_ids`, `unowned`, `appetite`, `source_types`, `institution_key`, `service_id`, `contract_id`, `finding_id`, `review_overdue`, `proposal_kinds` and `sort`.

### Response

```json theme={null}
{
  "items": [
    {
      "id": "cbrisk_...",
      "description": "Loss of the payments processor halts checkout",
      "status": "open",
      "inherent_score": 20,
      "residual_score": 12,
      "appetite": "outside",
      "owner": { "user_id": "cbuser_...", "display": "Dana Lee" },
      "signal_counts": { "open": 3, "closed": 1, "dismissed": 0 }
    }
  ],
  "total": 1,
  "limit": 25,
  "offset": 0
}
```

## Read the register summary and matrix

**method** `GET`

`GET /v1/risks/summary`

Returns the register's headline numbers: risks outside appetite, open proposals, unowned risks, overdue reviews, open signals, and counts by domain, source and category.

**method** `GET`

`GET /v1/risks/matrix`

Returns the likelihood by impact heat map with inherent and residual counts per cell.

**institution\_key** `string`

Limit either view to one institution of a multi-entity organization.

**risk\_domain\_id** `string`

For the matrix, limit to one risk domain.

## Create a risk

**method** `POST`

`POST /v1/risks`

Returns `201 Created` with the new risk and its computed scores.

**description** `string` *required*

What could happen.

**risk\_domain\_id** `string`

The risk domain the risk belongs to.

**likelihood\_level** `integer`

Inherent likelihood on your organization's scale. `impact_level`, `residual_likelihood_level` and `residual_impact_level` work the same way.

**owner\_user\_id** `string`

The owning user. Use `owner_department_id` to name a business unit instead.

**vendor\_ids** `string[]`

Vendors in scope. `service_ids`, `department_ids`, `entity_ids` and `region_codes` set the rest of the scope.

The body also accepts `category`, `status`, `treatment`, `treatment_plan`, `review_at`, `accepted_until`, `institution_key` and `note`.

## Get or edit a risk

**method** `GET`

`GET /v1/risks/{risk_id}`

Returns one risk with its signals, history and the excerpt of the dependency chain behind it.

**method** `PATCH`

`PATCH /v1/risks/{risk_id}`

Takes the same fields as create. Fields you omit keep their value. Returns the updated risk.

## Attach or dismiss a signal

**method** `POST`

`POST /v1/risks/{risk_id}/signals`

Returns `201 Created` with the attached signal.

**source\_type** `string` *required*

One of `finding`, `contract`, `contract_clause`, `contract_component_gap`, `obligation`, `screening_match`, `radar_signal`, `radar_alert`, `vendor_risk_change`, `inspect_evaluation`, `assessment` and `supplier_site`.

**source\_id** `string` *required*

The ID of the record.

**rationale** `string`

Why the record supports this risk.

**method** `POST`

`POST /v1/risks/{risk_id}/signals/{signal_id}/dismiss`

Dismisses the signal and returns it. The signal stays on the risk's history.

## Read exposure, the board report and the configuration

**method** `GET`

`GET /v1/risk-exposure`

Returns appetite gauges per risk domain, the loss model, the contractual cushion, and fourth party, business unit and geographic concentration. Pass `institution_key` to limit the domain gauges to one institution.

**method** `GET`

`GET /v1/risk-report`

Returns the board report: KRI tiles, movers, appetite breaches, domains, framework coverage and commitments on time. Pass `period` with a key from the response's `periods` to read an earlier period.

**method** `GET`

`GET /v1/risk-config`

Returns the Risk module configuration: appetite thresholds, taxonomy, quantification, likelihood and impact levels, domain appetite, signal sources and agent settings.

## Run and save scenarios

**method** `POST`

`POST /v1/risk-scenarios/run`

Runs a what-if scenario over the dependency chain and returns its blast radius, obligations, findings on the path and loss estimate. Nothing is saved.

**inputs** `object` *required*

The scenario. Set `kind` to `event`, `change` or `offboard`, name the subject with `subject_vendor_id` or `subject_node_id`, or send a plain-English `prompt` such as "Okta is down for 24 hours".

**method** `POST`

`POST /v1/risk-scenarios`

Returns `201 Created` with the saved scenario. Send `inputs`, an optional `title`, and the `result` of a run. When you omit `result`, Coverbase runs the scenario once and saves what it returns, which also needs `risk:run:org`.

**method** `GET`

`GET /v1/risk-scenarios`

Lists saved scenarios, newest first, with `limit` (at most 200) and `offset`.

**method** `GET`

`GET /v1/risk-scenarios/{scenario_id}`

Returns one saved scenario with its inputs and result.

## Manage risk domains

**method** `GET`

`GET /v1/risk-domains`

Lists your organization's risk domains with `limit` (at most 200) and `offset`. Pass `include_archived=true` to include archived domains.

**method** `POST`

`POST /v1/risk-domains`

Returns `201 Created` with the new domain. Coverbase records the change on the domain's activity feed.

**name** `string` *required*

The domain's name, 1 to 255 characters.

**description** `string`

What the domain covers, up to 1000 characters.

**weight** `number`

The domain's weight in the weighted roll-up, from 0 to 100.

**reviewer\_ids** `string[]`

Users who review the domain. Use `department_reviewer_ids` for user groups.

**method** `PATCH`

`PATCH /v1/risk-domains/{risk_domain_id}`

Takes the same fields as create. Fields you omit keep their value, and an explicit `null` for `weight` takes the domain out of the roll-up. To archive a domain, a signed-in user does it in the app.

## Read a vendor's risk score history

**method** `GET`

`GET /v1/vendors/{vendor_id}/risk-score-history`

Lists how a vendor's overall and per-domain risk scores changed, newest first, with `limit` (at most 200) and `offset`. An entry with a null `risk_domain_id` is the vendor's overall score.

## Read methodology and AI governance

These reads need the TPRM lifecycle. When your organization does not have it, they return `404` with the code `not_enabled`.

**method** `GET`

`GET /v1/risk-methodology`

Returns `irq_aggregation_method`, which decides how inherent risk questionnaire answers aggregate into a score.

**method** `GET`

`GET /v1/reviewer-matrix-rules`

Returns the reviewer matrix in `data`. Each rule names a requester group, risk level, risk domain and business unit (a null criterion matches anything) and one reviewing user or group.

**method** `GET`

`GET /v1/work-queue-escalation-rules`

Returns the rules that escalate overdue work queue items in `data`.

**method** `GET`

`GET /v1/ai-settings`

Returns PII redaction, learning, evaluation guidance, each agent policy and the privacy posture.

**method** `GET`

`GET /v1/agent-actions`

Lists the Agent Ledger, newest first. Filter with `awaiting_decision`, `target_type` and `target_id`, and page with `limit` (at most 200) and `offset`. Deciding an action stays with a signed-in user.

## Read access removal requests

**method** `GET`

`GET /v1/vendors/{vendor_id}/access-removal-requests`

Lists the identity-provider access removals a workflow proposed for a vendor, with each assignment it would remove. This read needs Inspect and returns `404` when Inspect is not enabled. Approving or rejecting a request stays with a signed-in user.

## Change configuration and methodology

**method** `PATCH`

`PATCH /v1/risk-config`

Changes the Risk module configuration and returns it. Only the fields you send change. An invalid level scale returns `422` with the code `invalid_risk_config`.

**method** `PUT`

`PUT /v1/risk-methodology`

Sets `irq_aggregation_method` and returns it.

**method** `POST`

`POST /v1/reviewer-matrix-rules`

Returns `201 Created` with the new rule. Send `requester_department_id`, `risk_level_id`, `risk_domain_id` and `business_unit_id` (a null criterion matches anything) and one of `reviewer_user_id` or `reviewer_department_id`. `PUT /v1/reviewer-matrix-rules/{rule_id}` replaces a rule with the same body.

**method** `POST`

`POST /v1/work-queue-escalation-rules`

Returns `201 Created` with the new rule. Send `name`, `overdue_days` (0 to 365), `target_type`, `item_types` (empty covers every type), `target_user_id` or `target_department_id`, and `reassign`. `PUT /v1/work-queue-escalation-rules/{rule_id}` replaces a rule with the same body.

**method** `PATCH`

`PATCH /v1/ai-settings`

Changes `pii_redaction_enabled`, `learning_enabled`, `evaluation_guidance` (null clears it) or `policies`, and returns the settings. A policy that always needs a person, or that no agent enforces yet, returns `400` with the code `policy_not_allowed`.

`GET /v1/ai-settings/learned-examples` lists the examples the org's reviewers taught control evaluation, newest first.

## Other reads and actions

`POST /v1/risks/{risk_id}/commitment-requests` asks a risk's owner to commit to treating it. Send an optional `due_at` and `message`. It returns `201 Created` with the `finding_id` and `commitment_id` it created.

`POST /v1/risk-chain/query` returns the dependency chain. Send the same filters the Risk page uses, such as `follow`, `depth`, `critical_only` and `focus_node_id`.

`GET /v1/access-removal-requests/{request_id}` returns one access removal request.

`GET /v1/vendors/{vendor_id}/due-diligence-status` returns `is_current`, the latest completed assessment and the next assessment date. It needs the TPRM lifecycle.

`GET /v1/findings-stats` returns the Findings page's insight numbers over every finding.

`POST /v1/commitments/{commitment_id}/extend-due-date` moves a mitigation-underway commitment's `due_at` later and stores an optional internal `note`. The vendor's acceptance stands. A commitment that is not mitigation-underway, or a date that is not later, returns `400` with the code `extension_not_allowed`.

## Questionnaire versions

These need the TPRM lifecycle. `GET /v1/questionnaire-versions` lists versioned templates; pass `types` (default `irq`). `POST /v1/questionnaires/{questionnaire_id}/draft-version` returns `201 Created` with a new draft copied from that version. `POST /v1/questionnaires/{questionnaire_id}/publish-version` publishes a draft so new sends use it.

## Responsibilities

These need Responsibilities enabled for your organization, and return `404` with the code `not_enabled` otherwise.

`GET /v1/responsibility-topics` lists the topics in template order. `POST /v1/responsibility-topics` returns `201 Created` with a new topic. Send `label`, `question`, and optionally `guidance`, `default_iaas`, `default_paas`, `default_saas` and `owner_department_id`.

`GET /v1/vendors/{vendor_id}/responsibility-matrix` returns the vendor's matrix, its current version and its version history. Pass `engagement_id` for an engagement's matrix. It returns `404` with the code `responsibility_matrix_not_found` until a matrix is saved.

`POST /v1/vendors/{vendor_id}/responsibility-matrix` saves `columns`, `rows`, `cells` and an optional `note` as a new published version and returns the matrix.

## Attestations and review sessions

`GET /v1/questionnaires/{questionnaire_id}/responses/{response_id}/attestation` returns the questionnaire's attestation config, every record on the response, and `is_satisfied`. Signing an attestation stays with a person.

`GET /v1/assessments/{assessment_id}/review-sessions` lists the remote and onsite review sessions on an assessment. It needs the TPRM lifecycle.

## Work queue items

The work queue holds tasks assigned to people, such as reviewing an assessment. A key reaches the items assigned to its own user; an admin user key reaches every item.

`GET /v1/work-queue-items` lists items, newest first. Filter with `statuses`, `types`, `vendor_id` and `assessment_id`, and page with `limit` (at most 200) and `offset`. `GET /v1/work-queue-items/{work_queue_item_id}` returns one item. An item the key cannot reach returns `403` with the code `work_queue_item_forbidden`.

`POST /v1/work-queue-items/{work_queue_item_id}/complete` marks an item complete. An item a record owns resolves with that record and returns `409` with the code `work_queue_item_conflict`. `POST /v1/work-queue-items/{work_queue_item_id}/reopen` puts a completed item back to pending.

`POST /v1/work-queue-items` returns `201 Created` with a new item and notifies its assignee. Send `title`, `description`, `instructions`, `url`, `vendor_id` and `assigned_user_id`, and optionally `type`, `assessment_id` (an assessment of the same vendor) and `due_at`. An unknown vendor, user or assessment returns `400` with the code `invalid_work_queue_item`.

`PATCH /v1/work-queue-items/{work_queue_item_id}` edits an item. Fields you omit keep their value, and an explicit null clears `due_at` or `assessment_id`. A new assignee is notified.

`POST /v1/work-queue-items/reassign` moves `work_queue_item_ids` to `assignee_id`, a user or a round-robin user group. A round-robin group spreads the items across its members. An assign-to-all group returns `422`. The response gives `reassigned_count` and the assignee.

## Finding acceptance rules

Acceptance rules decide who recommends, who decides and who approves when a finding's risk is accepted. The first rule that matches a finding's severity routes it. The rule endpoints need the TPRM lifecycle and return `404` with the code `not_enabled` otherwise.

`GET /v1/finding-acceptance-rules` lists the rules in the order they are checked.

`POST /v1/finding-acceptance-rules` returns `201 Created` with a new rule. Send `name`, and optionally `match_level_ids` (severity level IDs; empty matches every severity), `recommender_department_id`, `decider_department_id`, `extended_acceptance_days` with `extended_approver_user_id`, and `sort_order`. Without `sort_order` the rule is checked last. A user group or approver that does not exist, or a threshold sent without an approver, returns `400` with the code `invalid_rule`.

`PUT /v1/finding-acceptance-rules/{rule_id}` replaces a rule with the same body. A field you leave out is cleared, so send every value the rule should keep. `PUT /v1/finding-acceptance-rules/order` takes `rule_ids`, every live rule in the new order.

`GET /v1/findings/{finding_id}/decision-path` returns where a finding stands: `phase`, the routing rule in `route`, the `decisions` so far, and `can_recommend`, `can_decide` and `can_approve` for the key's user. Without the TPRM lifecycle it returns `enabled: false`.

Recording a decision and accepting a risk stay with a signed-in user in the app, and archiving a rule does too. The API does not offer them.

## More Risk module reads and actions

These need the Risk module, like the other `/v1/risk*` endpoints.

`GET /v1/risks/{risk_id}/signal-suggestions` lists findings, contract gaps, alerts and other records that look related to a risk and are not attached yet. Nothing is attached.

`GET /v1/risk-chain/nodes/{node_id}` returns one chain node: what it depends on, what it serves, contract terms, and the open findings, obligations and risks on it. Pass `snapshot_id` when you have it. An unknown node returns `404` with the code `not_found`. `POST /v1/risk-chain/rebuild` rebuilds the chain from current records and returns it.

`POST /v1/risk-chain/lineage` returns one row per business unit, service and vendor path, with its fourth party hops. Send optional `data_class_ids` and `department_ids`. It also needs TPRM expansion.

`POST /v1/risk-geography/query` groups supplier locations by country or region. Send `group_by`, an optional `focus_country`, and an optional vendor `filter` in the shape the Vendors page uses.

`GET /v1/risk-scenarios/subjects` lists the chain nodes a scenario can run against. Filter with `search`. `POST /v1/risk-scenarios/{scenario_id}/attach` links a saved scenario to `risk_id`. `POST /v1/risk-scenarios/create-risks` returns `201 Created` with the `risk_ids` it added from a scenario's `drafts`.

`POST /v1/risk-ask` answers a `question` about the register from your records, with citations. Send earlier turns in `history`. Nothing is saved.

`POST /v1/risk-agent/run` runs the risk agent now. It refreshes signals, attaches new ones and proposes risks. A person decides each proposal in the app.

`GET /v1/risk-narrative` returns the saved narrative for a `report_type` and optional `scope_key` or `subject_id`, or `null` when none is saved. `PUT /v1/risk-narrative` saves an edited `content` and marks it as edited by a person. `PUT /v1/risk-narrative/instructions` saves `custom_instructions` for the next generation.

## More methodology and AI governance

These need the TPRM lifecycle. The `org_settings` endpoints need a user key, as described above.

`GET /v1/vendor-tier-rule` returns the vendor tier rule, whether chain inputs are available, and the user group that approves a tier move. `POST /v1/vendor-tier-rule/preview` returns every vendor a `rule` would move, and changes nothing. `PUT /v1/vendor-tier-rule` saves the `rule` and then moves each vendor it changes; a null `rule` turns tiering off. The tier rule endpoints also need TPRM expansion.

`GET /v1/risk-methodology/scale-normalization` shows how each risk domain's control scale maps onto your risk levels, and the residual risk formula.

`POST /v1/reviewer-matrix-rules/reassign` gives every rule in `rule_ids` the same `reviewer_user_id` or `reviewer_department_id`, and returns `updated_count`.

`GET /v1/work-queue-escalation-log` lists every escalation the rules made, newest first. Filter with `escalation_rule_id` and page with `limit` and `offset`. `GET /v1/work-queue-items/{work_queue_item_id}/routing-events` shows how one item was routed, reassigned and escalated.

`GET /v1/agent-actions/{action_id}` returns one Agent Ledger entry. `GET /v1/agent-decision-rates` returns how often people accepted, edited or rejected what Coverbase AI proposed, per playbook, over the last `days` (1 to 365).

`PATCH /v1/ai-settings/learned-examples/{example_id}` turns one learned example on or off with `is_enabled`. `POST /v1/ai-settings/learned-examples/harvest` picks up reviewer overrides now instead of at the nightly harvest, and returns the examples.

## Control links, risk floors and what-if scores

`GET /v1/risk-domain-control-links` lists each risk domain's linked control sets and sections. Pass `include_archived=true` to include archived domains. `PUT /v1/risk-domains/{risk_domain_id}/control-links` replaces a domain's links with `control_set_ids` and `control_section_ids`. A set or section that does not exist returns `400` with the code `invalid_control_links`.

`GET /v1/vendors/{vendor_id}/residual-risk-floor` and `GET /v1/services/{service_id}/residual-risk-floor` return the significant deficiency that holds residual risk at a minimum, or `floor: null`.

`POST /v1/questionnaire-responses/{response_id}/irq-what-if` returns where an inherent risk questionnaire's score would land with some answers changed. Send `changes`, one `question_id` with its `option_ids` per answer. `POST /v1/assessments/{assessment_id}/residual-what-if` returns where an assessment's residual risk would land with the issues in `remediate_eval_ids` remediated. Both need the TPRM lifecycle and save nothing.

## Questionnaire versions and attestation settings

`GET /v1/questionnaires/{questionnaire_id}/change-log` lists who drafted, published, reviewed or retired each version of a template, newest first. `GET /v1/questionnaires/{questionnaire_id}/version-diff` compares a version with `base_questionnaire_id`. Without a base it compares with the published version, or with the version before when this one is published.

`PUT /v1/questionnaires/{questionnaire_id}/content-review` sets `interval_months` and `owner_user_id` for content reviews; a null `interval_months` removes the cadence. `POST` to the same path records that the content was reviewed today. These need the TPRM lifecycle.

`PUT /v1/questionnaires/{questionnaire_id}/attestation-config` sets what a questionnaire asks a respondent to attest to. Records already signed keep the statement they were signed against. An invalid config returns `400` with the code `invalid_attestation_config`.

Retiring a version stays with a signed-in user in the app.

## More responsibilities

These need Responsibilities enabled.

`PATCH /v1/responsibility-topics/{topic_id}` edits a topic. Fields you omit keep their value, and an explicit null clears `guidance` or `owner_department_id`. `PUT /v1/responsibility-topics/order` takes `topic_ids`, every live topic in the new order.

`POST /v1/vendors/{vendor_id}/responsibility-matrix/generate` starts AI Autofill and returns `202 Accepted` with a version in the `generating` state. To get the result, poll `GET /v1/vendors/{vendor_id}/responsibility-matrix/versions/{version_id}` until its `status` is `draft` or `failed`, then publish the draft with `POST .../versions/{version_id}/publish`. When the job cannot start, the version is marked failed and the call returns `503` with the code `generation_not_started`.

`POST .../versions/{version_id}/discard` deletes a draft that is not current. A service account key receives `403` with the code `api_key_not_allowed`. `POST /v1/vendors/{vendor_id}/responsibility-matrix/revert` copies `target_version_id` into a new published version. Each of these takes the same `engagement_id` query parameter as the matrix.

`PUT /v1/vendors/{vendor_id}/engagements/{engagement_id}/service-model` sets the engagement's `service_model` and fills every topic no document has settled with that model's default. `GET .../responsibility-acknowledgements` shows where each requested topic stands with its owner group. `GET .../unlinked-cuecs` lists the vendor's complementary user entity controls that no topic cites. `GET .../rights` returns the results of a linked assessment, such as a contractual rights review; pass `assessment_id` to pick one. `POST .../rights/scope-to-contracts` points the `assessment_id` at the engagement's contract documents only.

## Findings, commitments, obligations and assessments

`GET /v1/findings/{finding_id}/related-findings` lists the same third party's other open findings, newest first, and needs the TPRM lifecycle. `GET /v1/findings/{finding_id}/jira` returns whether Jira is connected and the ticket linked to the finding. `GET /v1/findings/{finding_id}/jira/draft` drafts a Jira ticket from the finding for you to edit and sends nothing. Both need TPRM expansion, and the draft also needs a connected Jira.

`POST /v1/commitments/{commitment_id}/regenerate-review` runs the AI review of the vendor's latest round again, with your current review instructions, and returns `202 Accepted`. When the latest round is not awaiting review it returns `400` with the code `review_not_pending`.

`POST /v1/obligations/bulk-action` applies one `action` to every ID in `obligation_ids` and returns `204 No Content`. The actions are `change_assignee` (with `assignee_id`, a user or user group), `change_status` (with `status`), `set_due_date` (with `due_date`, or null to clear it), `set_controls` (with `control_ids`) and `delete`, which archives. A service account key cannot use `delete`. Unknown IDs are skipped.

`POST /v1/obligations/extract-from-documents` queues a job that reads `vendor_document_ids` of `vendor_id` and adds the obligations it finds. It returns `202 Accepted` with `document_count`. Optionally send `obligation_type`, `service_ids`, `assignee_id` and `due_date` for every obligation it adds. The key's user is emailed when the job finishes.

`PUT /v1/assessments/{assessment_id}/method` sets `assessment_method`, such as remote or onsite. It needs the TPRM lifecycle.

`POST /v1/work-queue-items/complete` completes every pending item in `work_queue_item_ids`, or every item `filter` and `search` match. Items the key cannot reach and items a record owns stay pending. It returns `completed_count`.

## Review sessions

`POST /v1/assessments/{assessment_id}/review-sessions` returns `201 Created` with a new remote or onsite session. Send `title`, `mode` (`remote` or `onsite`), `scheduled_at`, and optionally `duration_minutes`, `location`, `notes`, `participant_user_ids` and `participant_emails`. Participant users are notified in the app, and the vendor's attendees in `participant_emails` are emailed. A time more than a day in the past returns `400` with the code `invalid_review_session`. These need the TPRM lifecycle.

`PUT /v1/assessments/{assessment_id}/review-sessions/{review_session_id}` replaces a session with the same body. When the time or place moves, every participant is told again. Otherwise only the people you added are told. Cancelling a session stays with a signed-in user in the app.

## Ask for responsibility acknowledgements

`POST /v1/vendors/{vendor_id}/engagements/{engagement_id}/responsibility-acknowledgements` asks each owner group to acknowledge its topics. Send the matrix `row_ids`, a `due_at`, and an optional `description`. Each group gets one Obligations Portal and one email that lists only its topics. The response gives each topic's status, `emails_sent`, `groups_without_members` (groups with nobody to email) and `groups_not_emailed` (groups whose email failed; their requests are saved). A selection that cannot be requested returns `422` with the code `invalid_acknowledgement_request`. It needs Responsibilities.

## Attestation documents

`GET /v1/questionnaires/{questionnaire_id}/responses/{response_id}/attestation/documents/{org_document_id}` returns a `url` that downloads a document the questionnaire asks respondents to read, and its `filename`. The URL expires after five minutes, and the response is never cached. A document the questionnaire does not name returns `404`.

## Extract findings from a document

To read findings out of an org or vendor document, start a run and poll it.

`POST /v1/documents/{document_id}/finding-extraction` returns `202 Accepted` with `status: pending`. Send optional `extra_instructions`, added after your organization's saved extraction instructions, and for a vendor document an optional `assessment_id` it must be linked to. A document that does not exist returns `404` with the code `document_not_found`. A system-generated assessment report, or a vendor document not linked to `assessment_id`, returns `422` with the code `extraction_not_allowed`.

`GET /v1/documents/{document_id}/finding-extraction` returns the run's `status` (`pending`, `running`, `completed` or `failed`), the `candidates` it found with their quotes and locations, and the `accepted_candidates`.

`POST /v1/documents/{document_id}/finding-extraction/accept` records `candidates` you accept. Send each one's `candidate_id` and `title`, and optionally an edited `description`, `finding_severity_level_id`, `finding_urgency_level_id`, `finding_risk_level_id` and `due_date`.

## Exports and imports

Exports run in the background. Each start call returns `202 Accepted` with the export's `id` and `status: queued`. Poll the matching `GET .../exports/{export_id}` until `status` is `complete`; the response then carries a `download_url` that expires after `expires_in` seconds. An ID from a different kind of export returns `404` with the code `export_not_found`.

`POST /v1/findings/exports?file_name=...` writes an Excel workbook of the findings the optional `filter`, `search` and `sort` in the body match.

`POST /v1/risk-chain/lineage/exports` writes the data lineage to a file. Send `export_filename`, `format` (`xlsx` or `csv`), and optional `data_class_ids` and `department_ids`. It needs the Risk module and TPRM expansion.

`POST /v1/agent-actions/exports` writes a CSV of the Agent Ledger actions an optional `filter` matches. It needs the TPRM lifecycle and a user key.

`POST /v1/responsibility-topics/import` creates or updates topics from an uploaded workbook, sent as multipart form data with `file` and an optional `dry_run`. Rows match existing topics by name. With `dry_run=true` nothing is saved, and the response shows what would change. A workbook that cannot be read returns `422`. It needs Responsibilities and a user key.

## AI narratives and suggestions

The app writes these texts as you watch. Through the API you start a job and poll it, since the model takes a few seconds to answer.

Each start call returns `202 Accepted` with the job's `id`, its `kind` and `status: pending`. Poll `GET /v1/text-jobs/{job_id}` until `status` is `completed` or `failed`. A completed job carries `result`; a failed one carries `error_message`. You can read only jobs you started.

`POST /v1/risk-narrative/jobs` writes a report's narrative and saves it as the report's narrative, as **Regenerate** does in the app. Send `report_type`, and optionally `scope_key`, `subject_id` and `regenerate`. Unless you set `regenerate`, a saved narrative that is still fresh, or one a person edited, comes back completed at once. Its `result` is the saved narrative: `content`, `title`, `generated_at` and `is_user_edited`. It needs the Risk module.

`POST /v1/finding-suggestions` suggests a finding's `title`, `description` or levels (`finding_levels_bundle`) from an assessment. Send `assessment_id`, `field_name`, `suggest_context` (`evals` or `document_requirement`), `eval_ids`, and optionally the `existing_finding_levels` already chosen. When every level is already chosen, the job completes at once. An unknown assessment returns `404` with the code `assessment_not_found`.

`POST /v1/vendor-finding-suggestions` makes the same suggestion for a vendor finding raised from a Radar alert. Send `vendor_id`, `field_name` and `radar` (`alert_id`, `severity`, `summary` and `detector_name`). An unknown vendor returns `404` with the code `vendor_not_found`.

`POST /v1/commitment-description-suggestions` suggests a commitment description. Send a `finding_id`, or `obligation_ids` with an optional `vendor_id`, and optionally `source` (`finding` or `obligation`). An unknown finding returns `404` with the code `finding_not_found`.

A suggestion's `result` is `{"text": ...}`: the model's answer inside `<output>`, with a `<value>` and a short `<reasoning>`, or the chosen level IDs for `finding_levels_bundle`. Nothing is saved on a finding or commitment; you apply the text yourself.

## Score history and AI action decisions

`GET /v1/services/{service_id}/risk-score-history`, `GET /v1/engagements/{engagement_id}/risk-score-history` and `GET /v1/assessments/{assessment_id}/risk-score-history` list how a record's overall and per-domain scores changed, newest first, with `limit` and `offset`, the same way the vendor history does. The assessment history needs the TPRM lifecycle.

`POST /v1/agent-actions/{action_id}/decision` records what you decided about one Agent Ledger action: `decision` is `accepted`, `edited` or `rejected`, with an optional `note`. `POST /v1/agent-actions/decisions` accepts or rejects many pending actions at once from `action_ids`; actions no longer waiting on a person come back in `skipped` with the reason. Deciding on an AI action is a person's call, so an API key gets `403` with the code `api_key_not_allowed`. Use a key created by a person whose role holds `org_settings:update:org`.

`POST /v1/commitments/query` returns the commitments that match a unified filter, search and sort, one page at a time. A key that reads only assigned commitments sees its own.

## Send commitments to a vendor

A vendor answers commitments on its persistent findings portal. These routes put commitments on that portal. None of them emails the vendor: to tell the vendor, invite them to the returned portal.

`POST /v1/vendors/{vendor_id}/findings-portal` opens the vendor's findings portal without adding anything to it.

`POST /v1/commitments/send-to-vendor` creates or updates a commitment per finding and puts them all on the vendor's portal, all or nothing. Send `vendor_id` and `items`, each with a `finding_id`, `description` and `due_at`, and a `commitment_id` to update an existing commitment. A finding of another vendor, or a commitment the vendor already locked, rejects the whole batch with `400`.

`POST /v1/commitments/send-obligations-to-vendor` puts a commitment per obligation in `obligation_ids` on the vendor's portal, due at `due_at`. An obligation's open commitment is reused.

## Edit a commitment and answer the vendor's round

`PATCH /v1/commitments/{commitment_id}` edits a commitment's terms, status or assignee. A field you omit keeps its value, and `null` clears it. Changing terms the vendor already accepted reopens their round on the portal, with your optional `revision_message`. A status change notifies the finding's people. Archiving or unarchiving with `is_archived` needs a user key; an API key gets `403` with the code `api_key_not_allowed`.

`POST /v1/commitments/{commitment_id}/resolve-round` answers the vendor's latest round with one of the two actions the commitment's `round_actions` offers:

| Action | What it does |
| - | - |
| `accept` | Closes the commitment. |
| `accept_risk` | Accepts the vendor's decline and closes the commitment. |
| `adopt_terms` | Takes the vendor's proposed due date; mitigation starts. |
| `request_more_work` | Rejects a completion; the round reopens and mitigation stays underway. |
| `re_request` | Rejects a decline or a compensating control; back to negotiating. |
| `hold_terms` | Keeps your terms and sends them back. |

A send-back reopens the vendor's round on the portal with your `message`. Nobody is emailed. `note` is kept as an internal comment. `accept`, `accept_risk`, `adopt_terms` and `request_more_work` are decisions, so an API key gets `403` with the code `api_key_not_allowed`. An action the round does not offer returns `400` with the code `round_action_not_available`.

## Commitment and finding documents

`GET /v1/findings/{finding_id}/documents` lists a finding's documents: its own, then each of its commitments'. A commitment's documents carry its `commitment_id`. `GET /v1/commitments/{commitment_id}/documents` lists one commitment's documents.

`POST .../documents` attaches a file as multipart form data with `file`, and an optional `note`. It returns `201 Created`. `PATCH .../documents/{document_id}` replaces the `note`; `null` clears it. `POST .../documents/{document_id}/archive` takes a document off. An uploaded document is archived, while a vendor document stays in the vendor's library. Archiving needs a user key.

A document never carries its storage location. `GET .../documents/{document_id}/download` returns a `url` to download it, valid for an hour, with `Cache-Control: no-store`.

## Create findings in bulk

`POST /v1/findings/bulk` creates up to 50 findings on one source. Send `source_type` (`assessment`, `vendor` or `org_document`), `source_id` and `rows`. Each row has your own `client_row_id` and a `title`, and optionally `body`, `assigned_to_id`, `vendor_id`, `due_date`, `eval_ids`, `finding_status_id` and the level IDs. A row from a document's finding extraction sends `approved_candidate_id`, and `extraction_source_document_id` on an assessment.

Each row lands or fails on its own. The response has the created findings in `data`, and each failed row's `client_row_id` and `reason` in `failed_records`. An unknown source returns `400` with the code `invalid_finding_source`. Assignees and the finding's people are emailed once each. The vendor is not.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.