> ## 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 billing and invoices

> How Fini answers billing and invoice questions from live billing data, runs routine billing changes through Actions, and hands refunds, disputes, and payment exceptions to your 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) resolves billing and invoice requests by reading each customer's live billing record through [User Attributes](/en/api-reference/attributes) and [Actions](/en/api-reference/actions), so replies quote the real balance, invoice number, payment status, and next charge date instead of generic policy text. Routine work (resending an invoice, explaining a charge, updating billing details) runs as a deterministic [Intent Rule](/en/automations/rulebook), while anything that moves money back to the customer or contests a charge goes to your team through [Reply Rules](/en/automations/reply-behavior) and escalation.

This page shows how to map a billing queue onto Fini's building blocks. It is a pattern to adapt, not a fixed recipe: your billing system, refund policy, and approval thresholds decide the exact tree.

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

Most billing volume is lookups and small, reversible changes. Those are safe to automate end to end. Requests that need judgment, touch payment credentials, or create a financial liability stay with people.

| Request | Who handles it | How |
| - | - | - |
| "Can you send me a copy of my invoice?" | Fini | A **Tool** node calls your `Resend Invoice` Action and the Reply confirms where it was sent. |
| "What is this charge?" / "Why is my bill higher this month?" | Fini | A lookup Action returns the invoice line items; the Reply explains them with real amounts. |
| "When is my next payment?" / "What's my balance?" | Fini | Read from the billing attribute on every message, no Action needed. |
| "My payment failed" | Fini | Explains the failure reason from your billing data and links to your self-serve payment page. Fini never collects card or bank details in chat. |
| "Change the billing email" / "Add our VAT or tax ID" | Fini | A **Form** collects the typed values, an Action writes them, the Reply confirms. |
| Proration, tax, and refund policy questions | Fini | Answered from your [Knowledge](/en/knowledge/overview) articles. |
| Refund requests | Your team (or Fini under a threshold you set) | Fini looks up eligibility and drafts the reply; a Reply Rules **Internal Comment** card posts it as an internal note for approval. See [Fini for refunds and returns](/en/use-cases/refunds). |
| Charge disputes, chargeback threats, suspected fraud | Your team | Escalated through the Planning Prompt's **Escalation Topics** before any rule runs. See [Fini for disputes and chargebacks](/en/use-cases/disputes). |
| Payment plans, hardship, fee waivers, collections | Your team | Escalated; these are policy exceptions, not workflows. |

```mermaid theme={null}
---
title: Who handles a billing request
---
flowchart TD
    MSG(["Billing message"]) --> RB{"Reply Rules"}
    RB -->|"No Reply:<br/>Human Agent Assigned"| SIL["Agent stays silent"]
    RB -->|"Internal Comment<br/>or Direct Reply"| ET{"Dispute, chargeback, fraud,<br/>collections or legal threat?"}
    ET -->|"Yes, Escalation Topics"| TEAM(["Your team"])
    ET -->|"No"| PICK{"Does the billing<br/>rule own it?"}
    PICK -->|"No, policy question"| KB["Answer from<br/>Knowledge articles"]
    PICK -->|"Yes"| FB{"Fallback root<br/>picks the branch"}
    FB -->|"invoice_request"| INV["Resend Invoice"]
    FB -->|"charge_question"| CHG["Get Invoice Details"]
    FB -->|"payment_failed"| PAY["Explain the failure,<br/>link the billing page"]
    FB -->|"billing_details_change"| DET["Form, then<br/>Update Billing Details"]
    FB -->|"No branch applies"| ASK["Ask which billing<br/>request they mean"]
    INV & CHG & PAY & DET & ASK & KB --> CARD{"Which card<br/>matched?"}
    CARD -->|"Internal Comment: refund_request<br/>or plan Equals enterprise"| TEAM
    CARD -->|"Direct Reply"| OUT(["Reply to the customer"])

    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,OUT,SIL source
    class KB,INV,CHG,PAY,DET,ASK agent
    class ET,PICK,FB,RB,CARD surface
    class TEAM human
```

<Tip>
  Start with the lookup intents. They carry the most volume, have no side effects, and give you a clean read on how well the billing attribute and tags work before you put any write Action behind the agent.
</Tip>

## How it works in Fini

A billing setup uses the same four pieces as every Fini workflow: an attribute for context, Actions for work, an Intent Rule for orchestration, and Reply Rules for overrides. Guardrails and Knowledge sit around them.

| Piece | What it does for billing | Where |
| - | - | - |
| **Tags** | A Rulebook-enabled group (for example `Billing Intent`) classifies each conversation as `invoice_request`, `charge_question`, `payment_failed`, `billing_details_change`, `refund_request`, or `other`. | [Tags](/en/configuration/tags) |
| **User Attributes** | A `Billing Account` attribute looks up the customer in your billing system on every message and exposes `billing_customer_id`, `plan`, `balance_due`, `next_charge_date`, `payment_status`, `last_failure_reason`, and `recent_invoices`. | [Attributes](/en/api-reference/attributes) |
| **Actions** | `Get Invoice Details` (read-only), `Resend Invoice`, and `Update Billing Details`, each with typed inputs and outputs. | [Actions](/en/api-reference/actions) |
| **Intent Rule** | A **Fallback**-rooted tree with one branch per billing intent, so one rule covers the whole billing queue. | [Intent Rules](/en/automations/rulebook) |
| **Reply Rules** | **Internal Comment** for refund requests and high-value accounts, **No Reply** when a human is already on the conversation. | [Reply Rules](/en/automations/reply-behavior) |
| **Business Rules** | Create the helpdesk ticket when a widget billing conversation escalates. | [Business Rules](/en/automations/business-rules) |
| **Guardrails** | Keep internal billing IDs out of replies and restrict links to your billing domains. | [Guardrails](/en/configuration/guardrails) |
| **Knowledge** | Billing policy articles (proration, taxes, refund windows, accepted payment methods) for every question the tree doesn't own. | [Articles](/en/knowledge/articles) |

### The billing attribute

Configure one attribute with a Data Collection Step that calls your billing system by the customer's email or account ID. Toggle each collected field deliberately:

| Field | Use in Rulebooks | Visible to AI | Why |
| - | - | - | - |
| `billing_customer_id` | ✓ | | Checks verify the customer is identified; Tool inputs bind to it. Never quoted to the customer. |
| `payment_status` | ✓ | ✓ | Checks branch on `past_due`; the agent can tell the customer their status. |
| `last_failure_reason` | | ✓ | Lets the agent explain a declined payment in plain language. |
| `balance_due`, `next_charge_date`, `plan` | ✓ | ✓ | Answers "what do I owe and when" without an Action call. |
| `recent_invoices` | | ✓ | Lets the agent ask "the March invoice or the April one?" when the customer doesn't name one. |

The same pattern is walked through field by field in [End-to-end: order status and changes](/en/walkthroughs/order-status-and-changes), where `recent_orders` plays the role `recent_invoices` plays here.

### Example behavior tree

<Note>
  **Example to adapt.** The tree below shows the shape of a billing rule. Tag values, field names, and Action names are placeholders; replace them with the ones in your workspace.
</Note>

```text theme={null}
Fallback  (root: one branch per billing intent)
├── Steps  (Invoice copy)
│   ├── Check: Billing Intent Equals invoice_request
│   ├── Check: billing_customer_id Is Not Null
│   ├── Read:  extract `invoice_id` (String) from the message and recent_invoices
│   ├── Tool:  Resend Invoice
│   │     in:  billing_customer_id, invoice_id
│   │     out: invoice_number, sent_to
│   └── Reply: confirm invoice ${invoice_number} was sent to ${sent_to}
├── Steps  (Charge explanation)
│   ├── Check: Billing Intent Equals charge_question
│   ├── Read:  extract `invoice_id` (String)
│   ├── Tool:  Get Invoice Details
│   │     in:  billing_customer_id, invoice_id
│   │     out: line_items, total, proration_note
│   └── Reply: explain the line items and ${total}; mention ${proration_note} if present
├── Steps  (Failed payment)
│   ├── Check: Billing Intent Equals payment_failed
│   ├── Check: payment_status Equals past_due
│   └── Reply: explain ${last_failure_reason}; share the self-serve payment page link
├── Steps  (Billing details change)
│   ├── Check: Billing Intent Equals billing_details_change
│   ├── Form:  billing_email, company_name, tax_id
│   ├── Tool:  Update Billing Details
│   │     in:  billing_customer_id, billing_email, company_name, tax_id
│   │     out: updated_at
│   └── Reply: confirm the new billing details apply from the next invoice
└── Reply: ask which billing request the customer means
```

<TreeWalker
  title="Try it: run the invoice copy branch"
  nodes={[
{ type: "Check", label: "Invoice branch: Billing Intent Equals invoice_request", onFail: "the Fallback skips this branch and tries the next one" },
{ type: "Check", label: "billing_customer_id Is Not Null", onFail: "the branch stops before any Action fires" },
{ type: "Read", label: "extract invoice_id from the message and recent_invoices" },
{ type: "Tool", label: "Resend Invoice", onFail: "the invoice branch fails and the Fallback moves on to the next branch" },
{ type: "Reply", label: "confirm invoice_number was sent to sent_to" }
]}
  scenarios={[
{ name: "Identified customer asks for a copy", outcome: "Resend Invoice runs and the reply confirms the invoice number and the address it was sent to, as your billing API returned them." },
{ name: "Customer not identified", stopAt: 1, outcome: "No Action fires. The other branches' intent Checks don't match either, so the last child asks which billing request the customer means." },
{ name: "Resend fails", stopAt: 3, outcome: "No confirmation is given. The other branches' intent Checks don't match, so the last child asks which billing request the customer means, and the AI Steps trace shows the failed Tool. Add a failure branch with a handoff Reply if you want your team to pick it up." },
{ name: "A charge question instead", stopAt: 0, outcome: "This branch is skipped and the Fallback runs the charge explanation branch, which calls Get Invoice Details." }
]}
  note="Example tree to adapt. This walks the first branch of the Fallback root; the other branches follow the same intent Check, then work, then Reply shape."
/>

How this runs: the root **Fallback** tries each branch in order, and each branch opens with an intent **Check**, so only the branch matching the conversation's `Billing Intent` tag runs. If the customer isn't identified, the `billing_customer_id` Check fails and the branch stops before any Action fires. The last child asks a clarifying question when no branch applies.

<Tip>
  **Failed payments end in a link, not a form.** Point customers to your billing portal to update a card or bank account. Don't build a Form or Read that collects payment credentials in the conversation; payment details belong in your payment provider's hosted page. 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**. Still keep card details out of the conversation by design and add [Guardrails](/en/configuration/guardrails) as an extra layer. See [Data handling](/en/security/data-handling#masking-sensitive-data) and [PCI DSS](/en/security/pci-dss).
</Tip>

The **Form** in the billing-details branch renders in the [Widget](/en/deploy/widget). On email channels, replace it with Reads in sequence, as described in the channel notes of [End-to-end: card replacement](/en/walkthroughs/card-replacement).

### Refunds under a threshold

If you want Fini to issue small refunds itself, add a refund branch that chains a read-only eligibility Tool before the refund Tool, and gate the write on the eligibility output (for example `refund_amount Less Than or Equal To 50`). The pattern is in [Rulebook → Chaining Tools](/en/automations/rulebook#chaining-tools). Everything above the threshold falls through to the internal-note path below.

## Guardrails and escalation

Billing mistakes are visible and expensive, so layer the controls:

<Steps>
  <Step title="Escalate disputes before any rule runs">
    In the Planning Prompt, add **Escalation Topics** for charge disputes, chargeback or "I'll contact my bank" language, suspected fraud on an invoice, collections, and legal threats. The planner routes these to a human and skips the billing rule entirely. See [Prompts → Controlling when the agent escalates](/en/configuration/prompts).
  </Step>

  <Step title="Hold refunds for review with Internal Comment">
    On **Rulebook → Reply Rules**, enable the **Internal Comment** card with `Billing Intent Equals refund_request`. Add an alternative group for high-value accounts, for example `plan Equals enterprise`. The agent still looks up eligibility and drafts the reply; your team sends it.
  </Step>

  <Step title="Stay silent when a human owns the ticket">
    Enable **No Reply** with `Human Agent Assigned Equals True` so the agent never talks over a billing specialist. No Reply wins over Internal Comment, which wins over Direct Reply.
  </Step>

  <Step title="Add reply checks">
    On **Guardrails**, add **Confidential attributes** for `billing_customer_id` and any internal risk or collections flags, a **URL allowlist** with your app and billing portal domains so the agent only links to real payment pages, and **Banned terms** for phrases your finance team never wants promised (for example "refund guaranteed").
  </Step>

  <Step title="Route escalations to the billing queue">
    For widget conversations, a [Business Rule](/en/automations/business-rules) creates the ticket in your helpdesk on escalation. For native tickets, map the billing tags to a billing team with [Agent groups](/en/configuration/agent-groups).
  </Step>
</Steps>

<Warning>
  Reply Rules gate only the final reply. If a refund Tool sits in the tree, the refund still executes when Internal Comment matches; only the confirmation is held. Keep write Actions that need approval out of the tree, or split the rule in two as described under "Holding the destructive Action itself" in [End-to-end: card replacement](/en/walkthroughs/card-replacement).
</Warning>

## What you need

| What | Why | Where |
| - | - | - |
| **A billing API** | Read endpoints for the customer and invoices, plus write endpoints for resending invoices and updating billing details. Follow the [API contract](/en/api-reference/api-contract) for endpoints you build. | Your billing system |
| **A credential scoped to those endpoints** | The Data Steps call your API with the credential in their Headers. Issue a dedicated key that can read invoices and perform only the writes the agent needs. | Your billing system |
| **A self-serve billing page** | The destination for payment-method updates and anything the agent should not do in chat. | Your app |
| **A Rulebook-enabled tag group** | The intent Checks in each branch read it. Turn on **Tag Group available in Rulebooks** and write AI Instructions that separate `charge_question` from `refund_request`. | [Tags](/en/configuration/tags) |
| **Billing policy articles** | The fallback for every question the rule doesn't own. | [Articles](/en/knowledge/articles) |
| **Sandbox customers** | Test invoices and a test payment failure to verify each branch without touching real accounts. | Your billing system |

## What to measure

Open [Analytics](/en/analytics) after the rule has run for a week:

* **Intent rule breakdown.** The billing rule's **AI Resolve Rate**, **Escalated Rate**, and **Volume**. A low resolve rate with a high escalated rate usually means a missing Action or a Check gated too tightly.
* **Tags filter.** Filter to each `Billing Intent` value to see which billing requests drive volume and which escalate.
* **Escalation reasons.** **Missing API Access** points at a billing request you haven't wired an Action for; **API or System Failure** points at your billing API; **Missing Knowledge** points at a policy article to write.
* **Conversation status.** Count **Resolved by AI**, not deflection. Deflection rate includes conversations still **Waiting for Customer**, such as a customer who was sent to the billing portal and never came back.

Before each change to the rule, run your billing test cases in [Test Suite](/en/testing/test-suite): one conversation per branch, with exact checks that the expected Action ran and an AI judgement that the reply quotes the amounts the Action returned. Test Suite uses recorded or mock Action responses, so runs never call your billing API; override a case's **Test setup** to try another outcome, such as a failed payment. For live end-to-end checks, use sandbox endpoints or a controlled pilot.

## Related

<CardGroup cols={2}>
  <Card title="End-to-end: order status and changes" icon="boxes-packing" href="/en/walkthroughs/order-status-and-changes">
    The closest walkthrough: a Fallback-rooted rule that handles several related intents with a shared lookup Action.
  </Card>

  <Card title="Intent Rules" icon="diagram-project" href="/en/automations/rulebook">
    Node types, chaining Tools, Forms, and testing a rule.
  </Card>

  <Card title="Reply Rules" icon="turn-down-right" href="/en/automations/reply-behavior">
    Internal Comment and No Reply conditions, and the priority order between them.
  </Card>

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

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

  <Card title="Guide: AI support for subscription billing questions" icon="book-open" href="https://www.usefini.com/guides/best-ai-support-tools-subscription-billing">
    The usefini.com guide to automating subscription billing questions.
  </Card>
</CardGroup>


## Related topics

- [Billing FAQ](/en/billing/billing-faq.md)
- [How Fini pricing works](/en/billing/how-pricing-works.md)
- [What counts as a resolution](/en/billing/what-counts-as-a-resolution.md)


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