Skip to main content

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.
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.

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:

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: 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 (SMSWHATSAPPVOICEEMAIL, ≤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.
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 for how the factor fits beside the OTP chain.