Skip to main content

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 verified or claimed and 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.

  1. First profile for this tenant from a genuine source → verify (verify_first_profile). Non-genuine first profile → stays unverified (first_profile_untrusted_source).
  2. First-party form submitted → verify (verify_form, reason first_party_form). Includes the widget pre-chat form (POST /api/intake, source: 'form').
  3. Booking made (visitor picked a slot, confirmed name/email/phone) → verify (verify_booking, reason first_party_booking). Bookings always auto-verify.
  4. CRM-tracked landing → verify (verify_crm, reason crm_tracked_landing) — unless the CRM contact was one we auto-created from enrichment (lead_crm_synced_at set), which is not proof of self-identification and stays claimed.
  5. Inbound call with caller-ID matching the person's phone → verify (verify_inbound_call, reason caller_id_match), only if accept_inbound_call_verification is on.
  6. Returning known device — the session id is already in the profile's verified set → verify (verify_session_id).
  7. Chat/call self-identification (visitor gives their identity in a chat or call, source chat/call) → verifies (verify_chat, reason chat_identification). This changed in the 2026-07 trust overhaul — it previously stayed claimed.
  8. Otherwise → score-based path (IdentityVerifierService.scoreClaim). The score path is refused outright on self-created CRM contacts (reason score_on_self_created_crm_contact).

Score-based path

Signals are summed into a score (weights from IdentityVerifierService.WEIGHTS):

SignalPointsNotes
Same kk_user_session_id (returning device)+100Effectively instant
FingerprintJS hash match+60Self-hosted FP library; 80–90% stable
Soft signature match+25sha256(userAgent + lang + screen + timezone + platform)
Exact IP within 30 days+20Recent same IP
/24 IP subnet within 30 days+10Same network (not added if exact-IP already hit)
Email already known to this tenant+15Only scores when a profile already exists
Phone already known to this tenant+15Only 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

PolicyScore thresholdDirect triggersUse case
permissiveAny (returns immediately verified on the score path)YesLegacy tenants; lowest friction
moderate≥ 60YesBalanced
strict≥ 80YesDefault for new tenants
regulatedScore path disabledYes (direct triggers only)Healthcare / finance / legal

Code note: permissive short-circuits to verified on the score path; regulated short-circuits to not verified on the score path (only direct triggers can verify). The numeric thresholds (moderate: 60, strict: 80) live in IdentityVerifierService.THRESHOLDS and 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. regulated exists 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_id on 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

  • IdentityVerifierServicescoreClaim() (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 on person_profiles; reverse IP index on person_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