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

> ## Agent Instructions
> Document only operations in the linked OpenAPI contract: the source-data ingestion operations and the source-scoped outbound webhook management operations.
> Treat the generated OpenAPI document as authoritative for request fields. Never invent compatibility fields, alternate endpoints, or additional source models.
> Preserve explicit nulls, decimal strings, timestamps, source identifiers, immutable version identifiers, and document-version links exactly as defined.

# Trigger a KYB run

> Start a KYB run for a business customer, poll its status, and receive signed lifecycle webhooks.

`POST /api/ingestion/customers/{customerId}/kyb-runs` starts a KYB run for a business Customer that
your ingestion source already created. Use it when the Customer and its documents are already in
Karsa and you want a fresh evaluation, for example after uploading a replacement document. To submit
a complete snapshot with related parties and provider evidence, use
[KYB dossiers](/guides/kyb-dossiers) instead.

## Address the customer

Every KYB run route accepts either form of customer reference. Both resolve only Customers owned by
the calling ingestion source.

| Form | Path |
| - | - |
| Karsa customer ID | `/api/ingestion/customers/{customerId}/kyb-runs` |
| Your `externalId` | `/api/ingestion/customers/by-external-id/{externalId}/kyb-runs` |

## Start a run

An `Idempotency-Key` header is required. The body is optional.

```bash theme={null}
curl -X POST "https://api.useasterisk.com/api/ingestion/customers/by-external-id/business-example-001/kyb-runs" \
  -H "Authorization: Bearer $KARSA_API_KEY" \
  -H "Idempotency-Key: kyb-run-business-example-001-2026-09-25" \
  -H "Content-Type: application/json" \
  -d '{}'
```

With an empty body, the run uses the Customer's current version and the current `AVAILABLE`
version of every ingested document linked to it or cited by its latest KYB dossier. Related parties
and provider evidence carry forward from that latest dossier. To choose the documents yourself, list
them with a KYB document role:

```bash theme={null}
curl -X POST "https://api.useasterisk.com/api/ingestion/customers/$CUSTOMER_ID/kyb-runs" \
  -H "Authorization: Bearer $KARSA_API_KEY" \
  -H "Idempotency-Key: kyb-run-$CUSTOMER_ID-formation-v2" \
  -H "Content-Type: application/json" \
  -d '{"documents":[{"documentVersionId":"document_version_example_01","role":"FORMATION_DOCUMENT","subtype":"CERTIFICATE_OF_INCORPORATION_OR_FORMATION","language":"en"}]}'
```

Karsa reads each document by its `subtype` (for example `CERTIFICATE_OF_INCORPORATION_OR_FORMATION`,
`UTILITY_BILL`, or `SHAREHOLDER_OR_MEMBER_REGISTER`, as in a KYB dossier) and `language` (a BCP 47
tag such as `en` or `pt-BR`; `und` is not accepted). A document cited by the Customer's latest KYB
dossier in the same role reuses that dossier's subtype and languages. Karsa does not guess: on a
first run, or for a document no dossier has cited, supply `subtype` and `language` in `documents`,
otherwise the request is refused with `422 KYB_DOCUMENT_METADATA_REQUIRED`. Each entry in its
`issues` has the `documentVersionId` as `path` and names what is missing.

Karsa answers `202 Accepted` with a `Location` header for the run:

```json theme={null}
{
  "id": "workflow_run_example_01",
  "status": "pending",
  "customerId": "customer_resource_example_01",
  "externalCustomerId": "business-example-001",
  "caseId": "case_example_01",
  "createdAt": "2026-09-25T12:00:00Z"
}
```

Replaying the same key and body returns the same `202` without starting another run. Reusing the key
with a different body, or for a different customer reference, returns `409 IDEMPOTENCY_CONFLICT`.
A Customer can have one active run at a time.

## Poll the run

```bash theme={null}
curl "https://api.useasterisk.com/api/ingestion/kyb-runs/workflow_run_example_01" \
  -H "Authorization: Bearer $KARSA_API_KEY"
```

```json theme={null}
{
  "id": "workflow_run_example_01",
  "status": "needs_review",
  "customerId": "customer_resource_example_01",
  "externalCustomerId": "business-example-001",
  "caseId": "case_example_01",
  "createdAt": "2026-09-25T12:00:00Z",
  "startedAt": "2026-09-25T12:00:02Z",
  "completedAt": "2026-09-25T12:02:10Z",
  "result": { "outcome": "ATTENTION_REQUIRED", "openIssueCount": 2, "caseId": "case_example_01" },
  "failure": null
}
```

List a Customer's runs, newest first, with `GET` on the same customer path. Pass `limit` (1–100,
default 20) and the returned `nextCursor` as `cursor` to read the next page.

## Status lifecycle

`status` is derived from the run and its Case.

| Status | Meaning | Terminal |
| - | - | - |
| `pending` | Queued; evaluation has not started. | No |
| `running` | Evidence is being gathered and evaluated. | No |
| `needs_review` | Finished with open issues; an analyst reviews the Case. | Yes |
| `completed` | Finished without open issues. | Yes |
| `failed` | Ended without a result; `failure.code` names the reason. | Yes |

A finished run has `result.outcome`, `result.openIssueCount`, and `result.caseId`. Karsa
evaluations report `ATTENTION_REQUIRED` or `NO_ATTENTION`; prepared cases report `APPROVED` or
`REVIEW_REQUIRED`; runs from the earlier KYB investigation keep `RFI_REQUIRED`,
`RESEARCH_REQUIRED`, or `HUMAN_REVIEW_REQUIRED`. A failed run has `failure.code`, for example
`KYB_EXECUTION_FAILED`. New statuses and outcomes may be added; treat an unknown value as not yet
terminal and keep polling or wait for a webhook.

## Errors

Errors use `application/problem+json` with a stable `code`. A trigger checks the Customer first, so
`CUSTOMER_NOT_FOUND` and `CUSTOMER_NOT_BUSINESS` are returned even while KYB evaluation is
unavailable.

| HTTP | `code` | Meaning |
| - | - | - |
| 400 | `INVALID_REQUEST` | The body, path, or required `Idempotency-Key` header is invalid. |
| 401 | `AUTHENTICATION_REQUIRED` | The bearer credential is missing or invalid. |
| 403 | `MACHINE_PRINCIPAL_REQUIRED` | The credential is not bound to an ingestion source. |
| 403 | `TENANT_NOT_ACTIVE` | The workspace is not active. |
| 404 | `CUSTOMER_NOT_FOUND` | Your source owns no Customer with that reference. |
| 404 | `KYB_RUN_NOT_FOUND` | Your source owns no KYB run with that ID. |
| 409 | `KYB_RUN_IN_PROGRESS` | A run is already pending or running; `existingRunId` names it. |
| 409 | `IDEMPOTENCY_CONFLICT` | The `Idempotency-Key` was used with a different request. |
| 422 | `CUSTOMER_NOT_BUSINESS` | The Customer's current version is not a business. |
| 422 | `KYB_DOCUMENTS_UNAVAILABLE` | No available ingested documents could be selected. |
| 422 | `KYB_DOCUMENT_METADATA_REQUIRED` | A selected document has no known `subtype` or `language`; `issues` lists them. |
| 422 | `INVALID_KYB_DOCUMENT_VERSION` | A selected document version is unknown, not yours, or not available. |
| 429 | `RATE_LIMITED` | Slow down and retry after the `Retry-After` header. |
| 500 | `INGESTION_INTERNAL_ERROR` | Karsa could not complete the request; retry later or contact support. |
| 503 | `KYB_RUNTIME_UNAVAILABLE` | KYB is not available in this deployment or not set up for this workspace. |

```json theme={null}
{
  "type": "https://karsa.app/problems/kyb-run-in-progress",
  "title": "Ingestion Rejected",
  "status": 409,
  "detail": "KYB run workflow_run_example_01 is already in progress for this customer",
  "code": "KYB_RUN_IN_PROGRESS",
  "requestId": "req_example_01",
  "existingRunId": "workflow_run_example_01"
}
```

## Webhooks

Subscribe an endpoint to `kyb.evaluation.queued`, `kyb.evaluation.completed`, and
`kyb.evaluation.failed` as described in [Set up outbound webhooks](/platform/webhooks). Each KYB
event's `data.kybRun` is the run exactly as `GET /api/ingestion/kyb-runs/{id}` returned it when the
event was recorded, so you can act on the webhook without polling. Events are delivered only to
endpoints that are active when they occur; to catch up after downtime or before an endpoint was
verified, list them with [`GET /api/ingestion/webhook-events`](/platform/webhooks#catch-up-on-missed-events).

```json theme={null}
{
  "id": "0f1f5a4e-4c0b-4d5f-9a55-3f2b6fb0d7a1",
  "type": "kyb.evaluation.completed",
  "version": "1",
  "createdAt": "2026-09-25T12:02:10Z",
  "source": { "id": "source_example_01", "key": "production-backend" },
  "subject": { "type": "customer", "id": "customer_resource_example_01" },
  "data": {
    "evaluationId": "assessment_example_01",
    "workflowRunId": "workflow_run_example_01",
    "caseId": "case_example_01",
    "supersedesWorkflowRunId": null,
    "status": "COMPLETED",
    "outcome": "ATTENTION_REQUIRED",
    "failureCode": null,
    "kybRun": {
      "id": "workflow_run_example_01",
      "status": "needs_review",
      "customerId": "customer_resource_example_01",
      "externalCustomerId": "business-example-001",
      "caseId": "case_example_01",
      "createdAt": "2026-09-25T12:00:00Z",
      "startedAt": "2026-09-25T12:00:02Z",
      "completedAt": "2026-09-25T12:02:10Z",
      "result": {
        "outcome": "ATTENTION_REQUIRED",
        "openIssueCount": 2,
        "caseId": "case_example_01"
      },
      "failure": null
    }
  }
}
```

A `kyb.evaluation.queued` event carries `kybRun.status` `pending`. A `kyb.evaluation.failed` event
carries `kybRun.status` `failed` and the same code in `failureCode` and `kybRun.failure.code`.
Deliveries are retried with the same event ID, timestamp, and signature, so deduplicate on the
signed body's `id`. The `Karsa-Webhook-Id` header is not signed: after verifying, reject a delivery
whose header ID or type differs from the body's `id` or `type`.

### Verify the signature

Karsa signs the UTF-8 bytes `timestamp + "." + rawBody` with Ed25519. Verify with the public key
for the delivery's `Karsa-Webhook-Key-Version` before parsing the JSON:

```ts theme={null}
import { createPublicKey, verify } from "node:crypto";

export function verifyKarsaWebhook(
  rawBody: Buffer,
  headers: Record<string, string>,
  publicKeyPem: string,
): boolean {
  const timestamp = headers["karsa-webhook-timestamp"];
  const signature = headers["karsa-webhook-signature"]?.match(/^v1=([A-Za-z0-9_-]+)$/u)?.[1];
  if (!timestamp || !signature || headers["karsa-webhook-version"] !== "1") return false;
  return verify(
    null,
    Buffer.concat([Buffer.from(`${timestamp}.`, "utf8"), rawBody]),
    createPublicKey(publicKeyPem),
    Buffer.from(signature, "base64url"),
  );
}
```


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