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

# Fini for KYC, onboarding and account changes

> How Fini answers verification-status and what's-missing questions from your onboarding system, makes account changes through your APIs, and leaves identity document review with your verification provider and team.

export const TreeWalker = ({title = "Try it: run the example tree", nodes = [], scenarios = [], note}) => {
  const FV = {
    lime: "#C3EE5E",
    ink: "#131415",
    line: "rgba(127,127,127,0.28)",
    soft: "rgba(127,127,127,0.07)",
    softer: "rgba(127,127,127,0.04)",
    muted: "rgba(127,127,127,0.95)",
    pass: "#C3EE5E",
    warn: "#FFB020",
    fail: "#FF4D4D",
    radius: 14
  };
  const fvCard = {
    border: `1px solid ${FV.line}`,
    borderRadius: FV.radius,
    padding: 18,
    margin: "20px 0",
    background: FV.softer
  };
  const fvChip = active => ({
    border: `1px solid ${active ? FV.lime : FV.line}`,
    background: active ? FV.lime : "transparent",
    color: active ? FV.ink : "inherit",
    borderRadius: 999,
    padding: "6px 12px",
    fontSize: 13,
    fontWeight: 600,
    cursor: "pointer",
    lineHeight: 1.2
  });
  const fvBtn = primary => ({
    border: `1px solid ${primary ? FV.lime : FV.line}`,
    background: primary ? FV.lime : "transparent",
    color: primary ? FV.ink : "inherit",
    borderRadius: 10,
    padding: "7px 14px",
    fontSize: 13,
    fontWeight: 600,
    cursor: "pointer"
  });
  const fvLabel = {
    fontSize: 11,
    fontWeight: 700,
    letterSpacing: "0.08em",
    textTransform: "uppercase",
    opacity: 0.6,
    marginBottom: 8
  };
  const [sc, setSc] = useState(0);
  const [pos, setPos] = useState(-1);
  const [running, setRunning] = useState(false);
  const cur = scenarios[sc] || ({});
  const stop = typeof cur.stopAt === "number" ? cur.stopAt : null;
  const last = stop === null ? nodes.length - 1 : stop;
  useEffect(() => {
    if (!running) return;
    if (pos >= last) {
      setRunning(false);
      return;
    }
    const t = setTimeout(() => setPos(p => p + 1), 650);
    return () => clearTimeout(t);
  }, [running, pos, last]);
  const run = () => {
    setPos(-1);
    setRunning(true);
    setTimeout(() => setPos(0), 50);
  };
  const pick = k => {
    setSc(k);
    setPos(-1);
    setRunning(false);
  };
  const done = !running && pos >= last && pos >= 0;
  const typeColor = {
    Check: "#E8E8E8",
    Read: "#E8E8E8",
    Form: "#E8E8E8",
    Tool: "#E8E8E8",
    Reply: FV.lime
  };
  return <div style={fvCard}>
      <div style={fvLabel}>{title}</div>
      <div style={{
    display: "flex",
    gap: 8,
    flexWrap: "wrap",
    marginBottom: 14
  }}>
        {scenarios.map((s, k) => <button key={k} style={fvChip(k === sc)} onClick={() => pick(k)}>{s.name}</button>)}
      </div>
      <div style={{
    fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
    fontSize: 13.5
  }}>
        {nodes.map((n, k) => {
    const reached = pos >= k;
    const failed = done || running ? stop === k && reached : false;
    const skipped = stop !== null && k > stop && (done || running);
    const state = failed ? "fail" : reached ? "pass" : "idle";
    return <div key={k} style={{
      display: "flex",
      alignItems: "stretch",
      gap: 10,
      opacity: skipped ? 0.35 : 1,
      transition: "opacity .3s"
    }}>
              <div style={{
      width: 18,
      display: "flex",
      flexDirection: "column",
      alignItems: "center"
    }}>
                <div style={{
      width: 2,
      flex: 1,
      background: k === 0 ? "transparent" : FV.line
    }} />
                <div style={{
      width: 12,
      height: 12,
      borderRadius: 999,
      background: state === "fail" ? FV.fail : state === "pass" ? FV.pass : FV.line,
      transition: "background .3s"
    }} />
                <div style={{
      width: 2,
      flex: 1,
      background: k === nodes.length - 1 ? "transparent" : FV.line
    }} />
              </div>
              <div style={{
      flex: 1,
      margin: "4px 0",
      padding: "8px 12px",
      borderRadius: 10,
      border: `1px solid ${state === "fail" ? FV.fail : state === "pass" ? FV.pass : FV.line}`,
      background: state === "pass" ? "rgba(195,238,94,0.14)" : state === "fail" ? "rgba(255,77,77,0.08)" : "transparent",
      transition: "all .3s"
    }}>
                <span style={{
      fontSize: 11,
      fontWeight: 700,
      padding: "2px 8px",
      borderRadius: 6,
      marginRight: 10,
      background: typeColor[n.type] || FV.line,
      color: FV.ink
    }}>{n.type}</span>
                {n.label}
                {failed && n.onFail && <div style={{
      fontFamily: "inherit",
      fontSize: 12.5,
      marginTop: 6,
      color: FV.fail
    }}>Short-circuits: {n.onFail}</div>}
              </div>
            </div>;
  })}
      </div>
      <div style={{
    display: "flex",
    justifyContent: "space-between",
    alignItems: "center",
    marginTop: 14,
    gap: 10,
    flexWrap: "wrap"
  }}>
        <button style={fvBtn(true)} onClick={run} disabled={running}>{running ? "Running..." : done ? "Run again" : "Run scenario"}</button>
        {done && cur.outcome && <div style={{
    flex: 1,
    minWidth: 220,
    fontSize: 14,
    padding: "8px 12px",
    borderRadius: 10,
    border: `1px solid ${cur.escalates ? FV.warn : FV.pass}`
  }}>
            <b>{cur.escalates ? "Escalates: " : cur.resolved ? "Resolved: " : "Outcome: "}</b>{cur.outcome}
          </div>}
      </div>
      {note && <div style={{
    fontSize: 12,
    opacity: 0.6,
    marginTop: 10
  }}>{note}</div>}
    </div>;
};

Fini (usefini.com) handles the support side of KYC and onboarding: it reads the customer's verification status and outstanding requirements from your onboarding or verification system through a User Attribute, explains what's missing and how to provide it using your Knowledge articles, and makes account changes (address, contact details, plan) through Actions that call your APIs. Identity document review, verification decisions and risk decisions stay with your verification provider and your compliance team; Fini reports the status those systems return and never decides it.

Most onboarding contacts are some version of *"why isn't my account verified yet?"* or *"what do you still need from me?"*. Those are answerable precisely when the agent can read the real status, which is what this pattern is built on.

## What Fini handles and what it hands to your team

| Fini handles | Your verification provider or team handles |
| - | - |
| Verification status questions, from the live status your system returns. | Reviewing identity documents and deciding whether verification passes. |
| Telling the customer exactly which items are outstanding, and where to submit them in your app or verification flow. | Risk, fraud and sanctions decisions, and any manual review queue. |
| Explaining requirements, accepted documents and timelines, from Knowledge articles your compliance team approves. | Exceptions to your onboarding policy. |
| Account changes through Actions, such as an address or contact-detail update with a validated Form. | Account changes your policy reserves for people, or that look like account takeover. |
| Re-running a status lookup after the customer says they've submitted something. | Customers disputing a verification decision. |
| Escalating when the status is stuck or the customer reports a problem with the verification flow. | Fixing verification flow issues with your provider. |

Identity verification and MFA resets work the same way. There is no separate feature to switch on: you build them as Agentic Actions, an Intent Rule plus Attributes and Actions that call your identity, KYC and MFA systems, so a verified customer can complete those steps with the agent instead of waiting for your team. For logged-in users there is also the widget's signed JWT identity ([Widget](/en/deploy/widget#identify-logged-in-users)). See the `Customer Identity` attribute pattern in [Card replacement](/en/walkthroughs/card-replacement). Because every step calls your own APIs and verification systems, the decision stays with the system that owns it.

## How it works in Fini

| Building block | What it does for KYC and account changes | Where |
| - | - | - |
| **User Attributes** | A `Customer Identity` attribute with a chained Data Collection Step that reads verification state from your onboarding system: for example `kyc_status`, `missing_items` and `last_reviewed_at`, alongside `account_id` and `account_status`. Turn on **Visible to AI** for `missing_items` and `kyc_status` so the agent can explain them; keep risk flags and internal reason codes off it. | [Attributes](/en/api-reference/attributes) |
| **Tags** | A custom Rulebook-enabled group (the example uses `Topic`) with tags such as `kyc_verification` and `address_change`. Default groups such as **Type of Issue** have a locked tag list. The [Tags](/en/configuration/tags) page uses `kyc_verification` in its fintech example. | [Tags](/en/configuration/tags) |
| **Actions** | Write Actions for each change you allow, for example `Update Address` or `Update Phone Number`, returning a confirmation id and whether re-verification is required. Optionally a read-only `Refresh Verification Status` Action for after the customer submits something. | [Actions](/en/api-reference/actions) |
| **Intent Rule** | A Fallback-rooted rule with one branch per intent: status, address change, contact change. | [Intent Rules](/en/automations/rulebook) |
| **Form** | Typed, validated input for account changes. The [address change example](/en/automations/rulebook#a-worked-example-address-change-with-form) is the canonical pattern. | [Intent Rules](/en/automations/rulebook) |
| **Knowledge** | Requirements, accepted documents, processing times and what each status means, written and approved by your compliance team. | [Knowledge](/en/knowledge/overview) |
| **Reply Rules** | **Internal Comment** or **No Reply** for changes on high-risk accounts. | [Reply Rules](/en/automations/reply-behavior) |
| **Guardrails** | **Confidential attributes** for fields whose values must never appear in a reply (for example a document number or date of birth, if your attribute returns them at all), and a **URL allowlist** so the agent only links to your own and your verification provider's domains. | [Guardrails](/en/configuration/guardrails) |

### Example behavior tree

This tree is an example to adapt. The status values, fields and Actions depend on your onboarding system.

```text theme={null}
Fallback  (root, picks the first branch whose intent Check passes)
│
├── Steps  (verification status)
│   ├── Check: Topic Equals kyc_verification
│   ├── Check: kyc_status Is Not Null
│   └── Reply: explain kyc_status in plain language; list missing_items;
│              point to where to submit them in your app
│
├── Steps  (address change)
│   ├── Check: Topic Equals address_change
│   ├── Check: account_status Equals active
│   ├── Form:  "Your new address"
│   │     ├── Field: street, city, postal_code, country (String, required)
│   │     └── Form Error: postal_code invalid for country
│   ├── Tool:  Update Address
│   │     in:  account_id, street, city, postal_code, country
│   │     out: confirmation_id, reverification_required
│   └── Reply: confirm the change and confirmation_id; if
│              reverification_required, explain the next step
│
└── Reply: ask whether the customer needs a status update or an account change
```

<TreeWalker
  title="Try it: run the address change branch"
  nodes={[
{ type: "Check", label: "Address branch: Topic Equals address_change", onFail: "the Fallback moves on to the last Reply, which asks what the customer needs" },
{ type: "Check", label: "account_status Equals active", onFail: "nothing is written, and the Fallback moves on to the last Reply" },
{ type: "Form", label: "Your new address: street, city, postal_code, country, with a Form Error for an invalid postal_code" },
{ type: "Tool", label: "Update Address", onFail: "the address branch fails, the form values are discarded, and the Fallback moves on to the last Reply" },
{ type: "Reply", label: "confirm the change and confirmation_id; if reverification_required, explain the next step" }
]}
  scenarios={[
{ name: "Active account, no re-verification", outcome: "Update Address runs and the reply confirms the change and the confirmation_id your API returned." },
{ name: "Re-verification required", outcome: "The change is made and the reply explains the re-verification step. Your API, not Fini, decided it was needed." },
{ name: "Account not active", stopAt: 1, outcome: "Nothing is written. The last Reply asks whether the customer needs a status update or an account change." },
{ name: "Update call fails", stopAt: 3, outcome: "No confirmation is given. The last Reply asks whether the customer needs a status update or an account change, and the AI Steps trace shows the failed Tool. Add a failure branch if you want a handoff here." }
]}
  note="Example tree to adapt. This walks the address change branch; the root Fallback tries the verification status branch first, and the diagram below shows every branch."
/>

How it runs:

1. Status questions never write anything. The branch reads the status the attribute fetched for this message and explains it. Because attributes fetch on every message, a customer who comes back after uploading a document gets the current status, not a cached one.
2. If `kyc_status` is null (the lookup failed or the customer isn't found), the status branch fails and the root Fallback moves on. The address branch's intent Check fails too, so the last Reply asks what the customer needs. To handle "we can't find your application" explicitly, add a branch for it (a Check on `kyc_status Is Null` and a Reply that explains the next step or hands off).
3. The address branch gates on an active account, collects the new address in a validated Form, and calls your API. Your API, not Fini, decides whether the change triggers re-verification, and the Reply reports it.
4. If `Update Address` fails, the address branch fails and the Fallback moves on to the last Reply; nothing confirms a change that didn't happen. To hand off instead, wrap the Tool and its Reply in a Fallback whose second child is a handoff Reply telling the customer a teammate will follow up.

```mermaid theme={null}
---
title: What Fini handles and what goes to your team
---
flowchart TD
    MSG(["Customer message"]) --> RR{"Reply Rules:<br/>high-risk account?"}
    RR -->|"No Reply"| TEAM(["Your team"])
    RR -->|"Direct Reply"| ET{"Account-takeover or<br/>identity-theft signal?"}
    ET -->|"Yes, Escalation Topics"| TEAM
    ET -->|"No"| FB{"Fallback root<br/>picks a branch"}
    FB -->|"kyc_verification"| ST{"kyc_status<br/>populated?"}
    ST -->|"Yes"| STR["Reply explains kyc_status<br/>and missing_items"]
    ST -->|"No"| ASK
    FB -->|"address_change"| ACT{"account_status<br/>active?"}
    ACT -->|"Yes"| FORM["Form collects<br/>the new address"]
    FORM --> UPD["Update Address<br/>through your API"]
    UPD -->|"Success"| CONF(["Reply confirms the change<br/>and any re-verification step"])
    UPD -->|"Fails"| ASK
    ACT -->|"No"| ASK["Reply asks: status update<br/>or account change?"]
    FB -->|"Neither"| ASK

    classDef source fill:#F7F7F7,color:#131415,stroke:#E8E8E8
    classDef agent fill:#131415,color:#FFFFFF,stroke:#131415,stroke-width:3px
    classDef surface fill:#FFFFFF,color:#131415,stroke:#131415
    classDef human fill:#C3EE5E,color:#131415,stroke:#131415,stroke-width:2px

    class MSG,CONF source
    class STR,FORM,UPD,ASK agent
    class ET,FB,ST,ACT,RR surface
    class TEAM human
```

Reply Rules evaluate before any Intent Rule. High-risk accounts are held by **No Reply** (no tree runs) or **Internal Comment**. Under Internal Comment the tree, including its Tools, still runs and the reply becomes an internal note, so gate the Tool itself with a Check for changes that must not happen automatically (see below).

## What you need

| What | Why | Where |
| - | - | - |
| **An authenticated channel** | Status and account details are personal data. Use the widget with a signed JWT, or the helpdesk's authenticated user. On unauthenticated channels, add an identity check before any account change, such as the one-time-code pattern described in the [Card replacement walkthrough](/en/walkthroughs/card-replacement). | [Widget](/en/deploy/widget) |
| **A status endpoint** | Read-only. Returns verification status and outstanding items in customer-explainable terms. If your verification provider holds this, expose it through your own API in front of the provider. | Your onboarding system or verification provider, called through an Attribute |
| **Account-change endpoints** | One per change you allow, each returning a confirmation id and any follow-up requirement. Should accept an idempotency key. Endpoints you build should follow the [API contract](/en/api-reference/api-contract). | Your core account system, called through Actions |
| **Credentials scoped to these operations** | Stored in the Data Step **Headers** JSON or from a connected integration's Connection Settings. Don't grant the key access to document images or verification decisions it doesn't need. | [Attributes](/en/api-reference/attributes), [Actions](/en/api-reference/actions) |
| **Approved requirement articles** | The agent explains requirements only from these. | [Knowledge](/en/knowledge/overview) |

<Note>
  **Keep document submission in your verification flow.** Point customers to the upload step in your app or your verification provider's flow rather than asking them to send identity documents in chat. That keeps document handling inside the system that reviews it and out of support transcripts. Fini automatically masks sensitive data, including card numbers and health details, everywhere it stores conversation data (transcripts, Inbox and **AI Steps** traces). Fields you hide from the AI are also redacted in **AI Steps**; Guardrails remain an extra layer on what the agent sends. See [Data handling](/en/security/data-handling).
</Note>

## Guardrails and escalation

Contact-detail changes are a common account-takeover path, so treat them as high-risk operations.

| Situation | Recommended behavior | How to configure it |
| - | - | - |
| Status is stuck past your stated processing time | Escalate with the status attached. | Have your status API compute a boolean such as `review_overdue` (Date operators compare against a fixed date, not a relative window), then add a branch with a Check on `review_overdue Equals True` and a Reply that hands off to your onboarding team. |
| Change of email or phone on a high-risk account | Hold for a human. | **No Reply** or **Internal Comment** in [Reply Rules](/en/automations/reply-behavior) with `is_high_risk Equals True` AND the change tag. The Tools in a rule still run under Internal Comment, so for changes that must not happen automatically, gate the Tool with a Check in the rule. |
| Account-takeover signals or identity theft | Escalate at the Planning step, before knowledge search. | *Escalation Topics* in the Planning Prompt; the [Prompts](/en/configuration/prompts) page lists account-takeover signals and sensitive data shared in-channel as triggers worth adding. |
| Customer shares an ID number or document details in chat | Don't repeat it, point to the secure flow. | **Confidential attributes** for any attribute values, and a Main Guidelines **Guardrails** instruction for what to say instead. |
| Customer disagrees with a verification decision | Hand to your team. | A tag for verification complaints, routed by an [Agent group](/en/configuration/agent-groups) or your helpdesk. |
| Customer asks for legal or regulatory advice | Decline and escalate if needed. | The "no medical, legal or financial advice" pattern in Main Guidelines **Guardrails**. |

## What to measure

Scope [Analytics](/en/analytics) with the **Tags** filter (your verification and account-change tags) or the **Intent rule** filter.

| Metric | Where | What it tells you |
| - | - | - |
| AI resolution rate for the intent | **Conversation Status** doughnut, share **Resolved by AI** | How many status and change conversations end without a human. |
| AI Resolve Rate and Escalated Rate | **Intent rule breakdown** for the rule; **Knowledge performance** for the requirement articles | Whether the rule or the articles are where conversations leak. |
| Human escalation rate | KPI card, filtered | Onboarding workload reaching your team. |
| Escalation reasons | **Escalation reason** filter | **Missing API Access** means the agent needed a system that isn't connected, **API or System Failure** that a status or change call failed; **Missing Knowledge** means a requirement isn't documented; **Escalation Constraint** is your own policy. |
| CSAT | **Average CSAT** and the **CSAT** filter | Onboarding friction as customers experience it. |

## Related

<CardGroup cols={2}>
  <Card title="Card replacement" icon="credit-card" href="/en/walkthroughs/card-replacement">
    Fintech walkthrough with chained attributes, a validated Form, a risk gate and the identity verification note for unauthenticated channels.
  </Card>

  <Card title="Order status and changes" icon="boxes-packing" href="/en/walkthroughs/order-status-and-changes">
    Step-by-step build of a Fallback-rooted rule with a status branch and a Form-based change branch.
  </Card>

  <Card title="Attributes" icon="id-card" href="/en/api-reference/attributes">
    Sources, chained Data Collection Steps, and the Use in Rulebooks and Visible to AI switches.
  </Card>

  <Card title="Guardrails" icon="shield-check" href="/en/configuration/guardrails">
    Confidential attributes, URL allowlist and custom reply checks.
  </Card>

  <Card title="Widget" icon="window-maximize" href="/en/deploy/widget">
    Identify logged-in users with a signed JWT.
  </Card>

  <Card title="Escalation and handoff" icon="headset" href="/en/use-cases/escalation-and-handoff">
    Where escalated onboarding conversations land and what your team sees.
  </Card>
</CardGroup>


## Related topics

- [Fini for password reset and account access](/en/use-cases/password-reset.md)
- [Setting up Fini for fintech and banking](/en/industry-setup/fintech.md)
- [Changelog](/en/changelog.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.