Verification Policy
Audience: Management, QA, support, engineering (security-relevant) · Where in app: Admin → Identity → Config (global default; per-company override) · Plan availability: All plans
Verification decides when the Identity Graph is allowed to surface a person's cross-session data to the AI. A session is either verified (the system trusts the identity → full memory) or claimed (someone typed something, but it isn't trusted across visits → current visit only). The policy sets how high the trust bar is.
There are two ways a session gets verified: an instant direct trigger, or a score that clears a threshold. The policy controls the score threshold and, in the strictest mode, turns the score path off entirely.
What it does
- Classifies each resolved session as
verifiedorclaimedand writes that onto the session (identity_trust,verified_person_id). - Gates whether the cross-session context block is assembled for the AI.
- Records an audit event on the PersonProfile explaining the decision (
identity_events[]), so "why did/didn't this verify?" is answerable later.
How it works
For each resolved session, PersonResolverService._decideTrust() runs in this order. A genuine-source gate applies first: only GENUINE_USER_SOURCES (form, chat_form, booking, inbound_call, chat, call) can verify — a first profile minted by any other source (enrichment, CRM auto-create) stays unverified with reason first_profile_untrusted_source. Enrichment can never mint verified.
- First profile for this tenant from a genuine source → verify (
verify_first_profile). Non-genuine first profile → stays unverified (first_profile_untrusted_source). - First-party form submitted → verify (
verify_form, reasonfirst_party_form). Includes the widget pre-chat form (POST /api/intake,source: 'form'). - Booking made (visitor picked a slot, confirmed name/email/phone) → verify (
verify_booking, reasonfirst_party_booking). Bookings always auto-verify. - CRM-tracked landing → verify (
verify_crm, reasoncrm_tracked_landing) — unless the CRM contact was one we auto-created from enrichment (lead_crm_synced_atset), which is not proof of self-identification and stays claimed. - Inbound call with caller-ID matching the person's phone → verify (
verify_inbound_call, reasoncaller_id_match), only ifaccept_inbound_call_verificationis on. - Returning known device — the session id is already in the profile's verified set → verify (
verify_session_id). - Chat/call self-identification (visitor gives their identity in a chat or call, source
chat/call) → verifies (verify_chat, reasonchat_identification). This changed in the 2026-07 trust overhaul — it previously stayed claimed. - Otherwise → score-based path (
IdentityVerifierService.scoreClaim). The score path is refused outright on self-created CRM contacts (reasonscore_on_self_created_crm_contact).
Score-based path
Signals are summed into a score (weights from IdentityVerifierService.WEIGHTS):
| Signal | Points | Notes |
|---|---|---|
Same kk_user_session_id (returning device) | +100 | Effectively instant |
| FingerprintJS hash match | +60 | Self-hosted FP library; 80–90% stable |
| Soft signature match | +25 | sha256(userAgent + lang + screen + timezone + platform) |
| Exact IP within 30 days | +20 | Recent same IP |
| /24 IP subnet within 30 days | +10 | Same network (not added if exact-IP already hit) |
| Email already known to this tenant | +15 | Only scores when a profile already exists |
| Phone already known to this tenant | +15 | Only scores when a profile already exists |
If the total ≥ the policy threshold, the session verifies (verify_score); otherwise it stays a claim. IP/subnet signals older than 30 days are ignored (networks rotate). The subnet check is IPv4-only.
Configuration & options
Set globally on IdentityGraphDefaultsModel.verification_policy; override per tenant via companies.identity_graph_config.verification_policy (requires identity_graph_use_custom_config = true).
The four policies
| Policy | Score threshold | Direct triggers | Use case |
|---|---|---|---|
| permissive | Any (returns immediately verified on the score path) | Yes | Legacy tenants; lowest friction |
| moderate | ≥ 60 | Yes | Balanced |
| strict | ≥ 80 | Yes | Default for new tenants |
| regulated | Score path disabled | Yes (direct triggers only) | Healthcare / finance / legal |
Code note:
permissiveshort-circuits to verified on the score path;regulatedshort-circuits to not verified on the score path (only direct triggers can verify). The numeric thresholds (moderate: 60,strict: 80) live inIdentityVerifierService.THRESHOLDSand are tunable without a migration.
accept_inbound_call_verification
Boolean (default true in the code defaults). When true, an inbound call whose caller-ID matches a person's stored phone verifies instantly. When false, the call still happens but the agent sees no cross-session context. Pair false with regulated for maximum strictness.
Worked example
A returning visitor cleared localStorage but is on the same Wi-Fi and same browser:
- FingerprintJS match: +60
- Same /24 subnet (no exact-IP hit): +10
- Soft signature match: +25
- Total: 95
Verifies under moderate (≥60) and strict (≥80). Fails under regulated (score path disabled) — they'd stay claimed until a direct trigger (e.g. a form submit) confirms them.
QA: reading a decision
Each decision lands as an identity_events[] entry on the PersonProfile, with type (e.g. verify_score, verify_form, verify_chat, claim), source, and score where applicable. Use this to answer "why is this session claimed not verified?" — it's almost always either policy threshold not met, an untrusted (non-genuine) source, or a direct trigger that didn't fire.
Behaviors & edge cases
- Verified can see everything; claimed sees only this visit. See the section-by-section table in Cross-Session Context.
- Score is a heuristic, not a security control. Do not use it for access-control decisions. Signals can collide on shared computers / corporate NATs.
regulatedexists precisely to remove that risk for sensitive tenants. - Self-created CRM contacts don't verify. A contact we minted from IP-enrichment (Snitcher/Apollo) is excluded from both the CRM direct trigger and the score path unless there's a genuine user action.
- Suspected ≠ claimed. Cross-device IP/device-signature guesses are shadow markers (
ip_shadow_person_id/device_shadow_person_idon the session), not a trust tier — they surface as the amber "Suspected" badge and never grant cross-session context. - Conflict guard. A session already verified for one Person rejects a later claim matching a different Person (audited as a rejected conflicting claim); it is not flipped.
- Verified signals are remembered. When a session verifies, its FP hash, soft signature, IP, subnet, and session id are stored (LRU-capped, last ~20 each) on the PersonProfile so a future session can be auto-verified by matching them.
- Signals degrade. FingerprintJS and soft signatures drift as browsers/devices update; IP signals expire at 30 days for scoring.
Plan & limits
Available on all plans. The policy itself is an operational setting, not a paid tier. The graph still requires an active subscription to assemble context regardless of policy. (Whether any plan tier forces a minimum policy: verify with product.)
Technical implementation
IdentityVerifierService—scoreClaim()(weights, thresholds, freshness),resolvePolicy(),acceptInboundCall(),computeSoftSignature(). Weights/thresholds are static and tunable.PersonResolverService._decideTrust()— owns the direct-trigger ordering and calls the verifier for the score path;_recordVerifiedSignals()persists the matchable signals and appends the audit event.IdentityGraphConfigService.getEffectiveConfig()— resolves the effective policy (defaults overlaid with per-company override), cached 60s.- Collections: policy on
identity_graph_defaults/companies.identity_graph_config; signals + audit onperson_profiles; reverse IP index onperson_ip_signals.
For resolution-chain internals and IP-assisted resolution see Identity Graph (subsystem).
What Nox can tell you
- The current verification policy (and per-company overrides)
- Why a specific session is/isn't verified (signal breakdown vs threshold)
- How many sessions are verified vs claimed
- The split of direct-trigger vs score-based verifications
Related
- Identity Graph Overview
- Cross-Session Context — what verified vs claimed unlocks
- Form Intercept — the most common direct trigger
- Admin Controls — where the policy is set
- Identity Graph (engineering subsystem)