# risk-webhooks

Source: https://docs.korala.ai/guides/risk-webhooks

---

# Risk push integration

Korala can notify your backend when risk evidence, assessments, tasks or overrides
change. Your backend does not need to poll continuously to discover these changes.
Use the notification as a reason to read current state, then apply your own account
or payment authorization rules.

## Connect an endpoint

In **Settings → Webhooks**, create an endpoint, select the **Risk** events you need,
and choose **Test** or **Live**. Alternatively, call `POST /api/v1/webhooks` with
`url`, `events` and `mode`. API-key subscriptions are pinned to the key’s environment.
Keep the returned signing secret on your server; it is not an API key.

| Event | What changed |
| --- | --- |
| `risk.subject.created` | A durable subject was registered. |
| `risk.policy.created` / `risk.policy.published` | A policy version was created or published. |
| `risk.activity.reported` | An integration recorded reported activity. |
| `risk.evidence.recorded` | Evidence was received, superseded or revoked. |
| `risk.assessment.created` | An assessment and its snapshot were committed. |
| `risk.action.requested` / `risk.action.transitioned` | A requirement was created or its status changed. |
| `risk.override.created` / `risk.override.revoked` | A reviewer decision was recorded or revoked. |

Only active, same-organization, same-environment subscriptions present when the
change commits receive deliveries. Adding a subscription does not replay older
history. Seed your initial view through the risk APIs, then use push notifications
and occasional reconciliation to maintain it.

## Payload and authentication

A notification contains resource references. It does not copy evidence values,
customer names, policy rules or reviewer reasons into the payload.

```json
{
  "eventId": "11111111-1111-4111-8111-111111111111",
  "event": "risk.assessment.created",
  "timestamp": "2026-09-28T10:00:00.000Z",
  "data": {
    "organizationId": "22222222-2222-4222-8222-222222222222",
    "subjectId": "33333333-3333-4333-8333-333333333333",
    "resourceId": "44444444-4444-4444-8444-444444444444",
    "sandbox": true
  }
}
```

`eventId` is the persisted risk event ID. It stays the same across retries and
subscribers. `resourceId` identifies the changed subject, policy, event, evidence,
assessment, action or override according to the event type. Policy notifications
have `subjectId: null`.

Use the original request bytes with `verifyWebhookSignature` from `@korala/auth`:

```typescript
const valid = verifyWebhookSignature(
  rawBody,
  request.headers['x-timestamp'],
  request.headers['x-signature'],
  webhookSecret,
);
```

Verify before parsing or processing. Check the signed body’s event, organization
and `data.sandbox` against your configured connection. The timestamp verification
rejects signatures outside the allowed time window. Do not authorize using the
`X-Webhook-Event` header alone; compare it with the signed body.

In production, commit a deduplicated event to a durable inbox before returning a
2xx response, then reconcile asynchronously. Acknowledge duplicates successfully.
Use `(connection, eventId)` as an inbox key and serialize updates for each subject
or otherwise prevent an older reconciliation from overwriting a newer one.

## Reconcile current risk

For `risk.assessment.created`, read
`GET /api/v1/risk/assessments/{resourceId}/state`. For evidence, activity, action or
override notifications, locate the subject’s latest assessment for each policy
purpose and read its state. Follow every `nextOffset` when listing history.

A completed collection task does not clear a review decision. New evidence can
make the earlier result stale before a replacement assessment exists. Respect
`current`, `effectiveDecision`, evidence freshness, review deadlines and override
expiry. `none` means no restriction under that policy, not blanket authorization.

Push events report committed changes. Monitored policies produce new assessments
after evidence expiry or a review deadline, then emit assessment events.
Unmonitored assessments and override expiry alone do not emit notifications.
Clients must still enforce recorded deadlines.

## Delivery behavior

Risk changes and their delivery records commit together. A worker dispatcher
checks the durable delivery records on a five-second schedule, in batches of 100;
queue load or worker downtime can delay delivery. A 60-second lease permits
recovery after interrupted enqueueing. Delivery uses the existing webhook queue:
five attempts, exponential backoff starting at 60 seconds, and a ten-second
outbound request deadline. Korala reads at most 4 KiB of each response and stores
a 1,000-character diagnostic preview. Failed deliveries remain visible and can be retried
from **Settings → Webhooks** or the retry API.

Delivery is at-least-once, not exactly-once, and ordering is not guaranteed.
Deduplicate even after a successful response: a process can stop after your
handler commits but before Korala records delivery success. Test records are
sent only to test subscriptions; mode is immutable on an existing subscription.

## Run a mock external client locally

The repository contains `scripts/mock-risk-client.mts`. It listens only on loopback,
verifies signatures and environment, deduplicates events, and uses a test API key
to fetch current state. It prints policy decisions without changing any account.
Its inbox is in memory and capped at 10,000 processed events; use a durable inbox
for a real integration.

1. Start the local API, Redis, PostgreSQL and worker. Use a disposable database and
   synthetic customers. For the **local API and worker only**, set
   `ALLOW_PRIVATE_NETWORK_REQUESTS=true` so loopback webhook delivery is allowed.
2. Create a **test** API key and a **test** webhook endpoint at
   `http://127.0.0.1:4040/risk-webhooks`. Select the risk events above and save the
   returned webhook signing secret privately.
3. Set the following variables in your local shell, then run the receiver:

```sh
export KORALA_LOCAL_API_URL=http://127.0.0.1:3005/api/v1
export KORALA_ORGANIZATION_ID=your-local-organization-id
export KORALA_TEST_KEY_ID=your-test-key-id
export KORALA_TEST_KEY_SECRET=your-test-key-secret
export KORALA_WEBHOOK_SECRET=your-webhook-signing-secret
pnpm exec tsx scripts/mock-risk-client.mts
```

4. In **Risk → Policies**, publish a synthetic policy accepting
   `api_key:<the key's database ID>` as an evidence source. This source ID differs
   from the public key identifier used for HMAC authentication.
5. Register a subject, submit evidence and request an assessment through the Risk
   API/SDK. The receiver prints the current state after the notification arrives.
6. Submit replacement evidence before requesting another assessment. The receiver
   should report `reassessment_required`. Reassess to produce a new current result.

The reproducible integration test provisions its own mock HTTP receiver and
subscriptions. It also verifies rollback, retries, duplicate and out-of-order
notifications, invalid signatures, and tenant/environment isolation:

```sh
# This suite clears the selected database: use a disposable migrated database.
TEST_DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:5432/korala_risk_test \
  REDIS_DB=12 RESEND_API_KEY='' \
  pnpm --filter @korala/api test test/integration/risk-webhook-client.spec.ts
```

See [Risk assessments](./risk) for configuration, evidence ingestion, reviewer
controls and the action lifecycle.

### Paging delivery history

Treat `nextCursor` as opaque and pass it unchanged when requesting the next page
for the same webhook and workspace mode. New cursors preserve deliveries with
identical creation timestamps. Legacy timestamp cursors remain accepted during
client upgrades. Invalid cursors and non-positive or non-integer limits return
HTTP 400; page size is capped at 100.
