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

# Verify console: fraud gate, fraud score, and fallback forensics

> Work the Fraud Score and Fraud Gate dialogs, read the code-attempts log and fallback execution timeline, and run the inline voice-biometrics module on the Verify dashboard.

# The Verify fraud-review console

The **Outbound → Verify** page is more than a send list. The header action row carries the **Fraud Score**, **Fraud Gate**, **Check verification**, **Bulk send**, and **Send verification** controls; the page fills in with the identity-risk signals panel, the fraud analytics, and (when your org has the voice-biometrics add-on) the voice biometrics section. The row-level **verification detail drawer** adds the channel-attempts table, the masked code-attempts log, and the fallback execution timeline.

This page walks through every surface: what opens the fraud-review state, how the score is composed, how to read the attempts log and the fallback chain, and where voice enrollment and verification live.

## 1. The fraud-gate surface

Verification enters the fraud-review state in two places, and the console treats them as siblings:

* **Fraud Score** (the narrow pre-screen) — a per-number dip over `POST /verify/fraud-score` that fuses SIM-swap recency, line-type, roaming, carrier, and country into a single 0–100 risk score, a `low` / `medium` / `high` band, and an advisory `allow` / `step_up` / `block` recommendation. Rate-limited to 20 dips per minute per tenant.
* **Fraud Gate** (the composite operator fusion) — a per-number dip over `POST /verify/fraud-gate` that fuses the SIM-swap recency, the port-event / number-recycling assertions, and (when you hold a 3-legged access token) the silent-network-auth possession verdict into one composite `allow` / `review` / `deny` decision. Rate-limited to 20 dips per minute per tenant.

Open either dialog from the Verify page header — pick a target, run the dip, and read the verdict block inside the dialog. The dialog locks its controls while the dip is in flight, so an operator cannot close or resubmit mid-check.

A verification that fails the composite gate lands in the **review** bucket: the dialog shows an amber `Review` verdict with the unresolved signal list rather than a clean `Allow`, so a missing operator dip never reads as a soft green. When the gate cannot reach a dip at all (no operator broker configured, a gateway timeout, or an invalid target) the page renders the same "partial signal" warning and forces the row into the review path rather than a fabricated clean verdict.

<Note>
  **Send-time vs. dip-time.** The send pipeline also runs a SIM-swap pre-flight synchronously — a blocked send returns `403 SIM_SWAP_DETECTED` with `last_swap_date` and `block_window_hours` and never creates a verification row. The Fraud Score and Fraud Gate dialogs described here are the operator-side console view of the same signal family, run on demand before you spend an OTP.
</Note>

## 2. Fraud score dialog + signals

The score panel tells you **why** the number scored the way it did. Each dip fills in the same structure:

| Field              | Meaning                                                                                                                                                                                                                                               |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `risk_score`       | 0–100 composite. Higher is riskier.                                                                                                                                                                                                                   |
| `risk_level`       | `low` / `medium` / `high` — the coarse band the UI colors on.                                                                                                                                                                                         |
| `recommendation`   | Advisory `allow` / `step_up` / `block`. The gate does not hard-block; your send flow decides.                                                                                                                                                         |
| `sim_swap`         | `swapped` (bool) and `risk_level`, plus `last_swap_date` when the operator answered. Tri-state: `Unavailable` is distinct from `No swap`.                                                                                                             |
| `line_type`        | `mobile` / `landline` / `voip` / `unknown`. A VOIP reading raises the score because OTP delivery to a non-carrier line is the classic interception vector.                                                                                            |
| `roaming`          | `yes` / `no` / `unknown`. `yes` nudges the score up; `unknown` renders as unavailable rather than as a clean `no`.                                                                                                                                    |
| `carrier`          | Operator name and type when the lookup resolved.                                                                                                                                                                                                      |
| `country`          | Resolved ISO country of the line.                                                                                                                                                                                                                     |
| `reasons`          | Machine-readable reason codes from the fusion — the list the drawer shows as mono chips.                                                                                                                                                              |
| `lookup_available` | Set to `false` when the number-intelligence lookup could not be run (invalid number, gateway timeout, no operator broker for the region). The score degrades to 0 / `allow` in that case — treat it as *no signal*, not as a confirmed-clean verdict. |

### How the score is composed

Two families feed the composite:

* **SIM-swap recency** — how recent a swap event is on this line. The closer a swap is to now, the higher the account-takeover risk. This is the single heaviest contributor.
* **Line-type / roaming / carrier** — a VOIP line, a currently-roaming line, or an unexpected carrier reads as higher-risk than an on-home mobile line from the expected carrier.

The gate's `headline_signals` block names the three operator-asserted families — `sim-swap`, `port-event`, `silent-auth` — and marks each as **resolved** or **unavailable** per dip. The unavailable list under the block enumerates the fusion keys the operator could not drink this turn (`numberVerified`, `simSwapped`, `portedRecently`, `numberRecycled`, …); an unavailable headline row renders in a muted tone and never as a green pass.

### Walk a verification through fraud analysis

1. The operator pastes a target into the **Fraud Score** field — `+14155550100`.
2. The dialog returns a score of 82, band `high`, recommendation `block`, with SIM-swap `swapped: true` and `last_swap_date` two days ago. The reasons list carries the swap-recency key, the line-type VOIP key, or both.
3. The operator opens **Fraud Gate** on the same target and confirms the composite verdict — if silent-auth possession is available (the caller holds a 3-legged token from the session), the gate adds it; if not, the gate still returns the fused verdict from SIM-swap + port-event alone.
4. With a `block` / `deny` verdict, the operator gates the send upstream (the send form stays closed); with `step_up` / `review`, the operator downgrades to a possession-proof channel (voice factor, silent-auth, or a re-push) instead of an SMS OTP.

## 3. Code attempts log

Open any verification row and the detail drawer shows two stacked tables.

**Channel attempts** — one row per channel the engine touched. Columns:

| Column        | Source                                                                                                                                                                                                                                   |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Provider      | The downstream carrier / route the send took                                                                                                                                                                                             |
| Channel       | `SMS` / `WHATSAPP` / `EMAIL` / `VOICE` / …                                                                                                                                                                                               |
| Attempted     | The send timestamp (tabular-nums, falls back to the raw ISO on unparseable input)                                                                                                                                                        |
| Status        | A canonical badge — `sent` / `delivered` (success green), `soft_error` / `throttled` / `timeout` (pending), `hard_error` / `failed` / `rejected` (error red), `expired` (expired amber) — same vocabulary as the list page's status pill |
| Error code    | Vendor-supplied machine code (e.g. a Telnyx `40010` or a DIDWW reject) when the route returned one                                                                                                                                       |
| Error message | The vendor-supplied message, when available                                                                                                                                                                                              |

**Code attempts** — the `/check` log. Columns: attempted timestamp, the submitted digits (masked to `****<last4>` by the backend — the frontend never applies its own masking, because that would imply the raw digits might transit), the result badge (`match` / `mismatch` / `expired` / `unknown`), and the source IP when the server captured one.

Use this table for fraud forensics: a `mismatch` row with the same IP hitting the same verification many times is the classic brute-force signature. The `masked` tag on the section header is deliberate — a detective view never exposes the OTP digits even to the operator.

## 4. Fallback timeline

The drawer's **Fallback execution** block renders the verification's append-only chain as a vertical rail ordered in execution time. Each step carries:

* The channel attempted (`SMS` → `WHATSAPP` → `VOICE` → `EMAIL`, ≤4 entries in practice)
* The outcome — `sent` / `delivered` (success), `soft_error` / `throttled` / `timeout` (warning), `hard_error` / `failed` / `rejected` (red), `skipped` / `pending` (muted)
* The attempt timestamp with timezone-aware rendering
* The vendor `error_code` and `error_message` pair when the route returned them, joined with a separator but never concatenated without one
* The provider message id under the route's `pmid:` prefix for downstream reconciliation

The rail resets to an explicit *"No fallback attempts recorded yet"* empty state on a single-channel send that never fell back — not a blank gap.

### How the timeline reorders on a carrier-gate rejection

`POST /verify/send` runs the order you supplied (or the profile's order). The chain advances a step when the previous step hard-fails on the carrier side, or when the engine's per-channel timeout `fallback_config.channel_timeout_seconds` elapses. A carrier gate that rejects a *voice* factor — the softswitch-softswitch pair rejects the call before any OTP plays — surfaces as a `hard_error` on the `VOICE` step with the vendor's error code attached, and the engine advances to the next step (`EMAIL` in a default `SMS → VOICE → EMAIL` chain) without re-trying voice.

Work through a real rejected voice hop:

1. The send opens on `SMS` — the provider returns `sent`, the SMS lands, and the rail step 1 paints success.
2. The recipient never enters the code. The engine's async-fallback timer fires and advances to step 2, `VOICE`. The route reaches the Devotel softswitch (the only outbound path), the carrier recognizes the number as a landline / VoIP-pair with no voice termination, and the softswitch returns a hard-reject code. The rail step 2 paints red `hard_error` with the vendor code and message.
3. The rail step 3, `EMAIL`, opens immediately. When the email provider accepts, step 3 paints `sent` then `delivered`. The verification remains `pending` — the failure of step 2 is logged, not terminal; only the final step's outcome decides the top-level status.

Operator tip: the rail's muted steps (`skipped` / `pending`) mean the engine did not have to run that hop at all — use them to distinguish "we ran out of channel budget" from "we never tried".

## 5. Voice biometrics — the inline module

When your org has the voice-biometrics add-on, the Verify page mounts the section inline below the fraud-signals panel. Three controls live on it:

* **Enrolled voiceprint count + recent events.** A strip of stat cards and a small event table showing the latest verification results (`passed` / `failed` / `deepfake_detected`) with the engine's confidence per call.
* **False-accept rate trend.** A 7-day sparkline of the rolling FAR so you can see threshold drift before you tighten the slider.
* **Settings card.** A confidence-threshold slider (0.50–0.95, permissive → strict) and an *Auto-2FA on low confidence* switch — when a voice check lands under the threshold, the engine falls back to the existing OTP chain rather than dropping the user. Saves go through `PATCH /api/v1/verify/voice-biometrics/settings`.
* **Enrolled-voiceprint ledger.** The cursor-paginated table behind `GET /api/v1/verify/voice-biometrics/` lists every enrolled print with its enrollment metadata (consent version, enrollment spoof score, sample duration, embedded model versions). This is the GDPR-erasure surface — an operator can erase any print from the dashboard instead of hunting a REST call.

The **Enroll** dialog opens from the section header; the actual enrollment and verification run over the `/api/v1/verify/voice-biometrics/*` endpoints from your app (Telephony leg or in-app capture). Verification runs against the tenant-set `confidence_threshold` — the same value the slider writes — and produces one of the three statuses above; the engine emits an audit record for every decision so the FAR trend you watch matches the ledger's history.

<Note>
  Voice biometrics is a **separate** surface from the OTP fallback chain — it is not a channel you can list under `channels[]` on a profile. Pair it with a verification profile; do not try to reorder it into a fallback step. See [Verify profiles](/guides/verify-fallback-chains) for how the factor fits beside the OTP chain.
</Note>

## Cross-links

* [Verify overview](/verify/overview) — the send model, channel list, and rate limits.
* [OTP approvals](/verify/otp-approvals) — the two-officer queue that gates templates and resend bursts.
* [Verify profiles: fallback chains](/guides/verify-fallback-chains) — the profile-side view of the fallback engine.
* [Verify webhook signatures](/guides/verify-webhook-signatures) — verify the event stream the console forensics surface is built on.
