
# Risk Intelligence webhooks

Risk Intelligence turns checks and observed activity into explainable assessment, signal, level and decision events. `risk.signal.created` is the supported signal event. There is no `risk.signal.detected` or signal-updated webhook.

## Events

| Event | When it fires | Recommended handling |
|---|---|---|
| `risk.assessment.completed` | An assessment finishes with a score, level and decision. | Upsert by `assessmentId`; use the decision for routing. |
| `risk.assessment.failed` | The assessment engine cannot produce a result. | Keep the subject retryable and inspect `failureCode`. |
| `risk.signal.created` | New durable risk evidence is recorded. | Append the immutable signal; do not overwrite earlier evidence. |
| `risk.level.changed` | Aggregate risk moves between levels. | Re-evaluate access and monitoring policy. |
| `risk.decision.changed` | The customer decision changes. | Apply the newest current decision after reading the resource if events arrive out of order. |

## Verification device and face findings

When verification observes a device shared across identities or a legacy face
finding without the opt-in workflow rule below, Myaza creates a review alert under **Risk Intelligence →
Investigations → Review Queue**. No custom signal policy is required. Subscribe your
organisation endpoint to `risk.signal.created` and `alert.created` in the same
environment as your verification.

These findings include `signalType` (`device.multi_entity` or
`biometric.face_match`; legacy `biometric.face_duplicate` is also supported), `signalId`, `verificationId`, `alertId` and
`subject.externalUserId`, your own customer reference. The signal recommends
review; it does not block an account, merge identities or confirm fraud. Match
images and other customers' identifiers are not included in the webhook.

Verify the webhook signature, store its event ID once, and use `externalUserId`
to route the right customer to your review process. Retryable delivery keeps the
same event ID. A compliance officer can inspect and resolve the alert using the
existing investigation workflow. More than one ordinary device is not, by itself,
a fraud finding; activity rules assess new-device and shared-device behaviour.

Face-reuse detection requires the deployed face model and eligible verification
captures. No match result is fabricated when the model or capture is unavailable.
The search runs after a successful verification, within the same organisation,
environment and face model, excluding the same entity. Production and development
captures are indexed by default. Sandbox stock personas are excluded unless the
deployment explicitly enables sandbox indexing. A missing finding does not prove
that an account is unique.

### Review one face across multiple accounts

Open **Workflow builder → Presence Intelligence → Rules**, turn on **Review face
reuse**, and set the number of **other accounts** that should trigger review.
Publish the workflow when ready. A threshold of 2 means at least two other
accounts: the current customer and repeated checks on one account do not count.

This edits the same rules used by Decisioning. For a custom graph, use
`verification.faceMatchedAccounts` (number) and `verification.faceSearchStatus`
(`complete` or `unavailable`). The Presence control also routes an unavailable
search to review. Similarity is evidence to investigate, not proof of fraud.

With this rule enabled, review stays on the verification. **No separate Review
Queue item is created.** A match sends `risk.signal.created` with
`reviewDestination: "verification"` and `recommendedAction: "follow_workflow_decision"`,
without an `alertId`. The final `workflow.run.completed` event includes the
workflow outcome, `externalUserId`, and a summary such as
`"faceReuse": { "status": "complete", "matchedAccountCount": 2 }`.
An unavailable search has a null count, not zero.

The deployed face engine and eligible captures must be available. Sandbox stock
personas are not evidence of real duplicates; indexing remains deployment-controlled.
Existing published workflows are unchanged until you enable and publish this rule.

### Apply the workflow decision in your platform

1. Send your stable `externalUserId` with each customer's verification. The raw
   verification result describes the identity check, not permission to keep using
   an account. A later face finding does not rewrite that result.
2. Subscribe to `risk.signal.created` for findings and `workflow.run.completed`
   for the final workflow outcome. Do not grant access based only on the earlier
   `verification.completed` event while Decisioning is still running.
3. For independent ongoing findings, a **signal-triggered policy** can match
   `biometric.face_match` (and the legacy alias where needed). Branch on
   `signal.type` and `signal.strength`, then use a review outcome or a step-up
   challenge. A similarity score alone is not proof of fraud and should not
   automatically merge customers or impose a permanent ban.
4. Subscribe to `workflow.run.completed` for your policy's outcome. Your backend
   applies your access policy using the customer reference. Review can mean a
   temporary hold; a confirmed decision can lead to a restriction. Myaza cannot
   disable a login, account or wallet hosted on your own platform.

Workflow completion notifications have durable intent saved with the completed
run and retry with a stable event ID. Notification replay does not reapply an old
disposition. Read the current customer/decision state before relaxing restrictions,
especially when events arrive late or out of order. A delivered outcome describes
that run, not proof that every later customer-state change has been applied.

Screening uses its own `screening.match` and `screening.completed` events; see
[Screening webhooks](/docs/webhook-screening).

## Complete assessment example

```json
{
  "version": "2026-08-11",
  "id": "evt_risk_assessment_01",
  "type": "risk.assessment.completed",
  "createdAt": "2026-08-15T10:42:13.914Z",
  "deliveryId": "del_risk_assessment_01",
  "data": {
    "riskAssessmentId": "ras_01j9risk001",
    "assessmentId": "ras_01j9risk001",
    "entityId": "ent_2405a1d091cf154b1ecfc9689e75f37b",
    "subjectType": "INDIVIDUAL",
    "score": 72,
    "level": "HIGH",
    "decision": "REVIEW",
    "checks": ["SANCTIONS", "PEP"],
    "signals": ["SANCTIONS_MATCH"]
  }
}
```

## Event-specific `data` contracts

```json
[
  { "type": "risk.assessment.failed", "data": { "riskAssessmentId": "ras_02", "assessmentId": "ras_02", "entityId": "ent_02", "subjectType": "BUSINESS", "failureCode": "provider_unavailable" } },
  { "type": "risk.signal.created", "data": { "entityId": "ent_02", "riskSignalId": "sig_01", "signalId": "sig_01", "assessmentId": "ras_02", "subjectType": "BUSINESS", "signalType": "SANCTIONS_MATCH", "strength": 0.91, "detectedAt": "2026-08-15T11:02:00.000Z" } },
  { "type": "risk.level.changed", "data": { "entityId": "ent_02", "subscriptionId": "msub_01", "previousLevel": "MEDIUM", "currentLevel": "HIGH" } },
  { "type": "risk.decision.changed", "data": { "entityId": "ent_02", "subscriptionId": "msub_01", "previousDecision": "REVIEW", "currentDecision": "BLOCK" } }
]
```

## Field reference

| Field | Type | Nullable | Values and meaning |
|---|---|---:|---|
| `riskAssessmentId`, `assessmentId`, `entityId`, `riskSignalId`, `signalId`, `subscriptionId` | string | Event-dependent | Stable customer-addressable resource IDs. `riskAssessmentId`/`assessmentId` and `riskSignalId`/`signalId` intentionally identify the same resources. |
| `subjectType` | string | No on assessment-originated events | `INDIVIDUAL` or `BUSINESS`. |
| `score` | number | Absent on failure | Normalised risk score. |
| `level`, `previousLevel`, `currentLevel` | string | Event-dependent | Current transition values such as `LOW`, `MEDIUM`, `HIGH` or `CRITICAL`. |
| `decision`, `previousDecision`, `currentDecision` | string | Event-dependent | `ALLOW`, `REVIEW` or `BLOCK`. |
| `checks`, `signals` | string[] | No on completed assessments | Checks completed and signal types produced by the assessment. |
| `failureCode` | string | No on failure | Stable diagnostic code. |
| `signalType` | string | No | Evidence class such as `NEW_DEVICE`, `IP_RISK`, `SANCTIONS_MATCH`, `BEHAVIOURAL_ANOMALY`, `COUNTERPARTY_RISK` or `WALLET_RISK`. |
| `strength` | number | Producer-dependent | Confidence from `0` to `1` for assessment-originated signals. |
| `severity`, `confidence`, `activityId`, `decisionId` | string, number, string, string | Producer-dependent | Present on behaviour or transaction-originated signals instead of `strength` and `assessmentId`. Severity is `LOW`, `MEDIUM`, `HIGH` or `CRITICAL`; confidence is `0` to `1`. |
| `detectedAt` | ISO 8601 string | No | Time the evidence was observed. |

## Semantics and related resources

Assessments are snapshots; signals are append-only evidence; level and decision events are material transitions. Ordering is not guaranteed, so read the entity risk resource before irreversible action. Screening, monitoring, fraud and alert events can refer to the same assessment without sharing a delivery order.
