# index

Source: https://docs.korala.ai/risk

---

# Independent risk framework

Risk subjects outlive verification sessions. Attach evidence to a person,
business, account or transaction, evaluate a published policy, and retain the
assessment and its explanation. A policy key identifies a purpose, such as
onboarding or fraud; one subject can have separate current assessments for each.

This API is independent of Korala's predefined session fraud evaluators.
It does not automatically approve identities, change account permissions or
deliver payment restrictions.

## Authentication and environments

Use the usual HMAC API credentials or dashboard JWT. Every resource belongs
to an organization and a test/live environment. API keys are pinned to their
mode; a test key cannot read or mutate live resources. Dashboard JWTs default
to live; send `X-Risk-Mode: test` to manage test resources. OAuth assistant
connections cannot use these endpoints yet.

Owner/admin JWTs are required to create/publish policies, create/revoke overrides
and cancel actions. Ordinary authenticated clients can record evidence,
evaluate published policies and acknowledge action execution. These operations
remain subject to organization and mode isolation.

Every mutation needs an `Idempotency-Key` header (1–120 letters, digits,
underscores, dots, colons or hyphens). Retry an identical operation with the
same key. Reusing a key with different input or a different principal returns
409. Successful retries return the same resource ID and its current data;
failed transactions do not consume the key.

## Resources

All paths below are relative to `/api/v1`.

| Operation | Endpoint |
| --- | --- |
| Create a subject | `POST /risk/subjects` |
| Create a draft policy version | `POST /risk/policies` |
| Publish a policy | `POST /risk/policies/{id}/publish` |
| Append evidence | `POST /risk/subjects/{id}/evidence` |
| Evaluate a policy | `POST /risk/subjects/{id}/assessments` |
| Read currentness/effective decision | `GET /risk/assessments/{id}/state` |
| Record an override | `POST /risk/assessments/{id}/overrides` |
| Revoke an override | `POST /risk/overrides/{id}/revoke` |
| Update an action | `POST /risk/actions/{id}/transitions` |
| List records | `GET /risk/{resource}` |
| Read a record | `GET /risk/{resource}/{id}` |

Resources are `subjects`, `policies`, `evidence`, `assessments`, `overrides`,
`actions` and `events`. Responses wrap the record as `{ id, resource, data }`.
List responses contain `items` and `nextOffset`; follow non-null offsets for
pages of at most 100 records and 1 MiB. A page can be shorter because of
its byte budget; always follow `nextOffset`, not the item count. Evidence, assessments, overrides, actions and
events support `?subjectId=...`. Policies and subjects do not support that filter.

The OpenAPI schemas describe each record's data. Assessment data includes the
policy snapshot, evaluation time, sequence, evidence revision, previous
assessment ID, exact input snapshots, score, band and factor/rule results.

## Subjects and evidence

Create a subject with a `kind` and your `externalId`. The combination is unique
within the organization/environment. Use stable customer/account identifiers;
do not put names or raw identity documents in the external ID.

Evidence is a typed scalar or a list of scalars:

```json
{
  "fact": "monthly_usage_units",
  "status": "available",
  "value": 32,
  "observedAt": "2026-09-27T09:00:00Z",
  "expiresAt": "2026-10-27T09:00:00Z",
  "providerRef": "report-123"
}
```

Observation times cannot be in the future. For `unavailable` or `revoked`
evidence, `value` must be null. Only an `available` observation can have a
non-null value. Evidence values are limited to strings, finite numbers,
booleans and lists of those types; structured provider payloads belong in
your evidence storage, referenced by `providerRef`.

The source is assigned by the server as `api_key:<database-key-id>` or
`user:<user-id>`. Read it from the resulting evidence record and explicitly
accept that source in a policy. A submitted provider name does not establish
trust. Key rotation currently requires updating accepted sources in a new
policy version.

New evidence supersedes the latest receipt for the same subject/fact/source.
The original record remains. A later receipt with an older observation time
still supersedes the previous receipt, so clients must deliberately reconcile
late events before submission. Revocation or unavailability never falls back
to older favourable evidence. Accepted sources that disagree make an input
unresolved. New evidence marks prior assessments stale.

## Resource limits

Oversized operations return 413. Limits are measured as UTF-8 JSON bytes:
64 KiB for a normalized policy request, 16 KiB for an evidence request,
256 KiB across selected evidence rows (including stored metadata), and
512 KiB for the evaluation result. Full-record reads are capped at 768 KiB.
Lists have a 1 MiB budget. These aggregate limits also apply when many small
requests have accumulated evidence; a rejected assessment leaves no partial
assessment/actions/audit event and does not consume its retry key.

## Policies

Create a version with `{ key, definition }`, inspect it, and publish it. Drafts
and published definitions are immutable through this API; creating another
version under the same key assigns the next version number. Assessments pin
an explicit published `policyVersionId`. Publishing a new version does not
silently reevaluate subjects or replace their historical policy.

The definition has these fields:

| Field | Meaning |
| --- | --- |
| `inputs` | Required facts, types, accepted sources and `maxAgeSeconds` |
| `applicability` | Optional groups of conditions that select which subjects the policy covers; a subject must match all conditions in at least one group |
| `factors` | Named factors with ordered condition/point cases, first/highest match, weight and optional default points |
| `aggregation` | Sum or maximum of weighted factor contributions |
| `floor`, `ceiling` | Bounds applied after aggregation |
| `bands` | Ordered IDs and lower bounds, each with an effect |
| `rules` | Conjunctive conditions with reasons, optional forced band and effects |
| `missingEvidence` | `awaiting_evidence` or `review` |
| `reviewAfterSeconds` | Assessment freshness deadline; monitored customers are reassessed when it elapses |

Conditions support scalar equality (`eq`), membership (`in`), list membership
(`contains`), numeric `gt`, `gte`, `lt`, `lte`, and comparison with another input
(`eq_fact`, `neq_fact`). For input comparisons, `value` names the other input key.
Types must agree with the
declared input. Rules contain `all`; use separate rules for alternative
conditions. Arbitrary scripts, expressions and network lookups are not allowed.

Factor `match: first` selects the first matching case. `highest` takes the
highest matched point value once, useful when an account declares multiple
access methods. Weights multiply factor points before aggregation. If no
case matches and there is no explicit `defaultPoints`, evaluation is incomplete.

Bands must start at zero and have strictly increasing lower bounds. They use
inclusive lower/exclusive upper boundaries; the final band is unbounded. A
score is not a probability. Do not compare scores across unrelated policies.

Effects contain a decision (`none`, `review`, `restrict`, `reject`) and an
array of named actions. Decision precedence is reject, restrict, review, none.
Forced bands only increase risk. Known mandatory rules still apply when other
inputs are missing. An incomplete assessment has a null numeric score.
`none` means this policy does not require a restriction; it is not KYC approval.

All policy limits and required properties are documented in the generated
`CreateRiskPolicyDto` schema. Nested group trees, maintained country-risk
tables and general arithmetic expressions are not supported in this version.

## Assessments and current decisions

Call the assessment endpoint with `{ policyVersionId, trigger }`. The service
evaluates persisted evidence and atomically stores its snapshot, explanation,
required actions and audit event. Replaying an assessment means using its
stored policy, inputs and evaluation time with the recorded evaluator version.

Historical GET always returns the original assessment. Before making a new
downstream decision, call its `/state` endpoint. It reports `current: false`
and `effectiveDecision: reassessment_required` if newer evidence arrived,
a newer assessment exists for the policy key, or a freshness/review deadline
passed. Refresh by explicitly creating a new assessment.

Risk changes support signed push notifications through [webhooks](./webhooks).
Enable automatic reevaluation by assigning a published policy through the monitoring endpoint below. On a
notification, fetch current action/assessment state before making a new decision. Do not treat
an old saved assessment or an action request as proof of current authorization.

## Human decisions and action history

An owner/admin may override a current assessment with `decision`, a nonempty
`reason` and an `expiresAt` within one year. The calculated score/band stays
unchanged. Incomplete evidence cannot be overridden to `none`. New evidence
invalidates the currentness of the assessment and the applicability of its
override. Only the most recent override may apply; revoking or expiring it
does not reactivate earlier overrides.

Actions start as `requested`. Consumers acknowledge them, carry out their
own authorized operation, then report completion. Send `status`,
`expectedRevision` and `reason` for each transition. Stale revisions return 409.

```text
requested → acknowledged → completed
    │             │
    └──→ failed ←─┘
           │
           └──→ requested (explicit retry)

requested / acknowledged / failed → cancelled (owner/admin only)
```

Completed and cancelled actions are terminal. A new low-risk assessment does
not automatically cancel earlier requirements. Each assessment can produce
its own action: reconcile existing open actions for the subject before
performing duplicate external work. Acknowledgements are client assertions,
not independent proof that a payment system applied a restriction.

`events` records authenticated actors and reasons for publication, evidence
changes, assessments, overrides and action transitions in the same transaction
as the change. It is application-level history, not a cryptographically sealed
audit log. There are no record deletion endpoints or configurable retention
jobs yet; organization deletion cascades its risk data.

## TypeScript client

```ts
const subject = await client.risk.createSubject(
  { kind: 'person', externalId: 'customer-123' },
  'customer-123-create',
);
const assessment = await client.risk.assess(
  subject.id,
  { policyVersionId: publishedPolicyId, trigger: 'onboarding' },
  'customer-123-onboarding-1',
);
const state = await client.risk.state(assessment.id);
```

Use `client.risk.inMode('test')` for owner/admin JWT sandbox governance and
reads. It returns a separate view and leaves `client.risk` in its default mode.
Selecting a contradictory mode with an API key returns 400; the credential
continues to determine its environment.

The methods also cover policy publication, evidence ingestion, overrides,
action transitions, list and get. Mutation keys are explicit so retries can
reuse them. Governance methods need an owner/admin JWT; normal API keys cannot
publish a policy or grant themselves override privileges.

## Korala check adapter and the next phase

`POST /risk/subjects/{id}/session-checks` accepts `{ checkId }` for an existing
persisted session-level check. It verifies tenant and subject relationships,
derives the fact as `check.<kind>`, and retains the submitting principal as source.
The source is the submitting `user:<id>` or `api_key:<id>`, which the policy
must explicitly accept. A persisted check is not a trusted provider attestation:
workflow authors can configure its provider and check kind. The early
`korala:session-checks` source is retired: it is excluded from new assessments,
and existing assessments using it require reassessment. Historical results
remain available unchanged.
Provider failures become unavailable evidence. Review/block flags do not
become a clear input. Participant-level checks require a separate adapter and
are refused. Imports require a session in the same test/live mode as the risk
subject. Test credentials cannot import a live session check.

The adapter imports evidence from an existing session. Configure accepted sources
and policy rules for your integration, then validate them in an isolated test
environment. Keep proprietary policy definitions and assessment data out of
public documentation and shared examples.

## Customer compliance history

Open **Risk → Customers** in the dashboard, select live or test, and choose a subject.
The profile shows recorded activity and assessment history. Select an event to
inspect its authenticated recorder, reported occurrence, receipt time, evidence
and linked assessments. Assessment details show the stored inputs and policy,
score contributions, rules, required actions and current applicability.

Your integration can record activity from a form, review process or external
service without moving that service into Korala:

```typescript
const event = await client.risk.recordActivity(subject.id, {
  activityType: 'information.submitted',
  category: 'customer',
  title: 'Supporting information submitted',
  summary: 'The client collected the requested declaration.',
  occurredAt: new Date().toISOString(),
  reference: 'your-submission-reference',
}, 'submission-record-001');

await client.risk.assess(subject.id, {
  policyVersionId: publishedPolicy.id,
  trigger: 'Review submitted information',
  eventIds: [event.id],
}, 'submission-assessment-001');
```

Activity reports provide context; they do not create evidence or prove that a
provider verified a claim. Submit typed facts through `addEvidence` or a supported
adapter before evaluating them. The server records the authenticated principal;
`category` describes the caller's report, not an authenticated customer identity.
One event can inform several assessments, and one assessment can reference up to
50 distinct events from the same subject and environment.

Use `client.risk.journey(subject.id)` to read up to 50 events in receipt order.
Pass the returned `nextCursor` as the second argument for older events. Use
`client.risk.eventAssessments(subject.id, event.id)` to inspect linked assessments;
follow `nextOffset` for additional results. Both reads respect response byte
limits. Use `client.risk.list('subjects', { search: 'customer-reference' })` for a
literal, case-insensitive search of external IDs.

The corresponding endpoints are:

| Operation | Endpoint |
| --- | --- |
| Record reported activity | `POST /risk/subjects/{id}/activities` |
| Read subject timeline | `GET /risk/subjects/{id}/journey?before={cursor}` |
| Read linked assessments | `GET /risk/subjects/{id}/events/{eventId}/assessments?offset=0` |

Mutations require an `Idempotency-Key`. The service refuses event references or
cursors belonging to another subject, organization or environment. API keys fix
the environment; dashboard JWT clients can use `client.risk.inMode('test')`.

A policy action can request information such as a source-of-funds declaration.
Your workflow chooses how to collect it: your own interface, Korala capabilities,
an external provider or manual review. Record requests, submissions and review
outcomes separately. Completing a collection action does not approve a customer;
submit the resulting evidence and request a new assessment. The framework does
not automatically create forms or initiate provider requests.

## Configure and operate risk in the dashboard

**Risk → Policies** lists policy versions in the selected test/live environment.
Owners and administrators can create a version with evidence inputs, accepted
sources, factor cases and weights, score bands, required actions, mandatory
rules, aggregation and freshness settings. Save to validate a draft; then review
and publish it. Copying a saved version creates a new draft. Saved
versions are not edited in place. Integrations pin the published version ID;
publication does not automatically migrate customers to it.

**Risk → Review queue** lists required actions across customers, oldest first.
Search a customer reference, filter by policy key or task status, and select a
task to inspect its originating assessment. Every status change requires a
reason and the current revision. Owners/admins can cancel a task; other allowed
transitions remain available to workspace members. This is an action queue:
a review decision without a configured action does not create a queue entry.
A newer assessment with lower risk leaves existing tasks open.

On the customer page, **History type** separates reported activity, assessments,
evidence, required actions and overrides. Korala filters each history on the
server before pagination. Assessment history has its own pagination; selecting
an activity shows the assessments linked to that exact event. The original
result and current applicability are displayed separately.

In assessment details, reviewers can reassess current evidence using
the same policy version. An owner/admin can apply or revoke a time-limited
override with a reason. Neither operation edits historical scores. Completing
a source-of-funds task is not a substitute for submitting evidence and
reevaluating it.

Configure push endpoints in **Settings → Webhooks**, choosing the **Risk** event
group and the matching environment. See [risk push integration](./webhooks).

## Automatic reassessment

Assign one published version per customer and policy key. Publishing a new version
leaves existing assignments unchanged. The customer page provides the same
monitor, pause and resume controls as the API.

```ts
await korala.risk.configureMonitoring(subjectId, {
  policyVersionId,
  enabled: true,
}, crypto.randomUUID());
const subscriptions = await korala.risk.monitoring(subjectId);
```

Use `POST /risk/subjects/:id/monitoring` with an idempotency key and
`GET /risk/subjects/:id/monitoring` to inspect assignments. To pause, submit the
same version with `enabled: false`. Assigning another published version with the
same policy key replaces the monitored version.

The worker checks due subscriptions every five seconds, up to ten per tick.
Backlogs and worker downtime increase latency. It reassesses after evidence
changes, native verification changes, evidence expiry and `reviewAfterSeconds`.
Unchanged evidence does not create a new result on each tick. Subject evidence
revisions also invalidate assessments conservatively when an unrelated fact
changes. Activity-only events do not trigger evaluation.

Known mandatory rules run with partial evidence: accepted age 15 can produce
`decision: reject`, `status: incomplete` and `score: null` while address evidence
is missing. The final numeric score requires all configured inputs. Missing or
expired evidence cannot produce an unrestricted complete result.

If a policy’s applicability conditions are missing or do not match, monitoring
records `applicability_unknown` or `applicability_not_matched` and produces no
assessment. Inspect `lastError`, `lastEvaluatedAt` and `nextCheckAt`; the latter is
an eligibility time, not a delivery guarantee. `evaluation_failed` retries after
30 seconds. A database outage can delay that retry further.

Assessment creation, required actions, webhook staging and the monitoring
checkpoint commit together. A duplicate worker tick does not duplicate that
assessment. A later assessment can request the same action again; consumers
must reconcile existing requirements. Webhooks still have at-least-once delivery.

Continue checking current assessment state and enforcing freshness deadlines
before authorizing a consequential action. Pausing monitoring does not revoke
existing assessments or cancel actions. Overrides retain their own expiry; an
expired override alone does not trigger an assessment or notification.

## Accepting KYC before the whole session finishes

Native input bindings normally use the completed, approved linked session.
An owner/admin may explicitly review a completed workflow attempt while another
workflow, such as proof of address, is pending:

```http
POST /risk/subjects/:id/verification-decisions
Idempotency-Key: unique-review-command

{
  "sessionWorkflowId": "<linked-workflow-slot-id>",
  "workflowRunId": "<current-completed-run-id>",
  "decision": "approved",
  "reason": "Reviewer accepted this verification attempt"
}
```

Supported decisions are `approved`, `rejected` and `revoked`. This endpoint
requires an owner/admin dashboard JWT. API credentials cannot impersonate a
Korala verification reviewer; external providers submit evidence through their
own authenticated source instead.

The decision belongs to the persisted customer, session workflow and exact run.
It does not approve the overall session. A newer attempt cannot inherit it;
revocation does not resurrect an earlier approval. Processing completion alone
is insufficient. The native adapter still checks workflow version, tenant, mode,
output provenance and freshness. An adverse or expired session prevents use of
favorable native facts. Each decision appears in the customer’s audit history.
