> ## 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 cancellations and subscriptions

> How Fini handles subscription cancellations, pauses and plan changes through your billing API, with confirmation before destructive steps and review gates for high-value accounts.

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 subscription cancellations end to end in a deterministic Intent Rule: it confirms the customer actually wants to cancel, identifies their account, captures the reason, calls your billing API through an Action, and replies with the effective date, refund amount and confirmation id your system returned. The same pattern covers pauses and plan changes, and high-value accounts can be held for human review with an internal note instead of a direct reply.

The step-by-step build lives in the [Cancellation flow walkthrough](/en/walkthroughs/cancellation-flow), which is Fini's canonical end-to-end example. This page covers the decisions around it: what to automate, what to keep with your team, how to confirm before acting, and what to measure.

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

| Fini handles | Your team handles |
| - | - |
| Cancellation policy questions (notice periods, what happens to data, refunds on cancel), answered from Knowledge articles. | Accounts you've reserved for people, for example Enterprise contracts or customers with an active balance. |
| Confirming intent before anything irreversible runs. | Retention conversations you want a person to have, such as a call before a large account leaves. |
| Identifying the customer and their plan (User Attributes). | Cancellations the billing API refuses for reasons that need judgment. |
| Capturing the cancellation reason, free text (Read) or structured (Form). | Reversing a cancellation, which is an operation in your billing system, not in Fini. |
| Cancelling, pausing or changing the plan through Actions, and confirming the real effective date and amounts. | Disputes about charges already made (see [Disputes and chargebacks](/en/use-cases/disputes)). |
| Nudging a customer who went quiet mid-flow, with an inactivity follow-up. | |

## How it works in Fini

| Building block | What it does for cancellations | Where |
| - | - | - |
| **Tags** | A `cancel_request` tag in a custom Rulebook-enabled group (the example uses `Topic`; default groups such as **Type of Issue** have a locked tag list), with a "when not to apply" section that separates a request from a policy question (`cancellation_inquiry`) or venting. Optionally a confirmation group (*Action confirmation* with `user_confirmed`, `user_denied`, `not_applicable`) that the rule reads before cancelling. | [Tags](/en/configuration/tags) |
| **User Attributes** | `Customer Identity`, exposing `customer_id`, `plan` and `is_vip`. Turn on **Use in Rulebooks** for all three; turn on **Visible to AI** only for `plan`. | [Attributes](/en/api-reference/attributes) |
| **Actions** | `Cancel Subscription`, with `customer_id` required and `reason` optional, returning `confirmation_id`, `refund_amount` and `effective_date`. Add `Pause Subscription` or `Change Plan` the same way if you offer them. | [Actions](/en/api-reference/actions) |
| **Intent Rule** | Gates on intent, confirms, cancels and replies. | [Intent Rules](/en/automations/rulebook) |
| **Reply Rules** | **Internal Comment** for `is_vip Equals True` AND `Topic Equals cancel_request`, the walkthrough's review gate (the walkthrough uses the value `Cancellation`). | [Reply Rules](/en/automations/reply-behavior) |
| **Knowledge** | Articles for the policy and the fall-through paths: account not found, account already closed, what happens if the cancel call fails. | [Knowledge](/en/knowledge/overview) |

### Example behavior tree

The walkthrough's tree cancels as soon as the intent matches. The variant below adds an explicit confirmation step, the first defensive pattern the walkthrough recommends. It's an example to adapt: tag names and fields are placeholders.

```text theme={null}
Steps  (root)
├── Check: Topic Equals cancel_request
├── Check: customer_id Is Not Null
├── Read:  extract `reason` (String)
└── Fallback
    ├── Steps  (customer confirmed)
    │   ├── Check: Action confirmation Equals user_confirmed
    │   ├── Tool:  Cancel Subscription
    │   │     in:  customer_id, reason
    │   │     out: confirmation_id, refund_amount, effective_date
    │   └── Reply: confirm effective_date, refund_amount and confirmation_id
    ├── Steps  (customer changed their mind)
    │   ├── Check: Action confirmation Equals user_denied
    │   └── Reply: acknowledge, confirm the plan stays active
    └── Reply: summarize what cancelling the current plan means
               and ask the customer to confirm
```

<TreeWalker
  title="Try it: run the example cancellation tree"
  nodes={[
{ type: "Check", label: "Topic Equals cancel_request", onFail: "the rule does nothing, and policy questions are answered from Knowledge" },
{ type: "Check", label: "customer_id Is Not Null", onFail: "the rule stops without calling your billing API" },
{ type: "Read", label: "extract reason" },
{ type: "Check", label: "Confirmed branch: Action confirmation Equals user_confirmed", onFail: "the Fallback tries the user_denied branch, then the last Reply" },
{ type: "Tool", label: "Cancel Subscription" },
{ type: "Reply", label: "confirm effective_date, refund_amount and confirmation_id" }
]}
  scenarios={[
{ name: "Customer replies: yes, cancel it", outcome: "The confirmation tag is applied before the rule runs, so the first branch cancels and the reply confirms the effective date, refund amount and confirmation id your billing API returned." },
{ name: "First message, not confirmed yet", stopAt: 3, outcome: "Nothing is cancelled. The last Reply summarizes what cancelling the plan means and asks the customer to confirm, and the conversation waits for the customer." },
{ name: "Customer changes their mind", stopAt: 3, outcome: "Nothing is cancelled. The user_denied branch acknowledges and confirms the plan stays active." },
{ name: "Customer can't be identified", stopAt: 1, outcome: "The Steps short-circuits, so the rule does nothing on this message and your billing API is never called. To ask for the registered email instead, wrap this Check and the nodes below it in a Fallback whose second child is an asking Reply." }
]}
  note="Example tree to adapt, not a default configuration. Steps inside the Fallback are labelled with their branch."
/>

How it runs: on the first message, the confirmation tag isn't `user_confirmed` yet, so the Fallback reaches the last Reply and asks the customer to confirm. Each customer message re-runs the rule from the root, so when the customer replies *"yes, cancel it"*, the confirmation tag is applied during tagging (before the rule runs) and the first branch cancels. If the customer goes quiet after the confirmation question, a **Follow up after inactivity** setting on that Reply can send one reminder while the conversation is **Waiting for customer**.

```mermaid theme={null}
sequenceDiagram
    autonumber
    actor C as Customer
    participant F as Fini
    participant API as Your billing system<br/>via an Action
    actor T as Your team

    C->>F: I want to cancel
    Note over F: Intent and customer Checks, Read reason
    F->>C: Summarizes what cancelling means and asks to confirm
    Note over C,F: Waiting for customer, with an optional inactivity follow-up
    C->>F: Yes, cancel it
    Note over F: Tagging applies user_confirmed, the rule runs again from the root
    F->>API: Cancel Subscription with an idempotency key
    API-->>F: confirmation_id, refund_amount, effective_date
    alt is_vip Equals True
        F->>T: Internal note with the drafted confirmation
        T->>C: Follows up first
    else Everyone else
        F->>C: Confirms the effective date, refund amount and confirmation id
    end
```

If you prefer an explicit button, use a **Form** with a single confirmation field before the Tool instead, as the walkthrough describes. Pair either approach with an idempotency key in the Action body built from values you have before the call (for example `${customer_id}-cancel`; `effective_date` is a Tool output, so it isn't available yet) so a duplicate message can't cancel twice.

### Pauses, downgrades and retention offers

Common adaptations, each a sibling branch under a Fallback root (the multi-intent shape from the [Order status and changes walkthrough](/en/walkthroughs/order-status-and-changes)):

* **Pause instead of cancel.** A `pause_request` branch calling `Pause Subscription` and replying with the resume date it returns.
* **Downgrade.** A `plan_change` branch with a Check on `plan`, then a `Change Plan` Tool.
* **Retention offer.** If your billing system exposes an offer endpoint, a Tool that fetches the offer for this customer runs before the confirmation question, and the `user_denied` branch applies it. Keep offer logic in your system, so the agent only presents what your API returned.
* **Different review rules.** Replace `is_vip Equals True` with `plan Equals enterprise`, or switch the Reply Rules card to **No Reply** to leave those customers entirely to your team.

## What you need

| What | Why | Where |
| - | - | - |
| **A billing API** | A lookup for the customer and a cancel endpoint (plus pause or plan-change endpoints if you offer them). Endpoints you build should follow the [API contract](/en/api-reference/api-contract). | Your billing system, called through an Action |
| **Customer identification** | Email or a customer token passed by the widget JWT, UI metadata, or the connected helpdesk, used as the attribute input. | [Widget](/en/deploy/widget), [Attributes](/en/api-reference/attributes) |
| **Credentials scoped to subscription operations** | Stored in the Data Step **Headers** JSON (for example an `Authorization` header). | [Actions](/en/api-reference/actions) |
| **Idempotency support** | Each message re-runs the rule; a repeated cancel must return the same result. | Your billing API |
| **Tags** | The intent tag, and the confirmation group if you use it, both with **Tag Group available in Rulebooks** on. | [Tags](/en/configuration/tags) |
| **A sandbox** | Test Suite runs use recorded or mock Action responses, so they never cancel a real subscription. For a live end-to-end check of the cancel Action, point it at a sandbox environment or run a controlled pilot. | [Test Suite](/en/testing/test-suite) |

## Guardrails and escalation

| Situation | Recommended behavior | How to configure it |
| - | - | - |
| VIP or Enterprise customer cancels | Run the flow, draft the confirmation as an internal note so a human can follow up first. | **Internal Comment** card in [Reply Rules](/en/automations/reply-behavior). The cancel Tool still runs; if it must not, stop the tree before the Tool for these customers with a Check on `is_vip`. |
| Account with an active balance or contract term | Hand to a human. | A Check in the rule that routes to a branch ending in a handoff **Reply** that tells the customer a teammate will follow up, or an *Escalation Topics* entry such as "account closure with active balance" in the Planning Prompt. |
| Customer threatens to leave for a specific reason | Let the planner escalate on judgment. | *Escalation Topics* for fuzzy patterns; Reply Rules for hard predicates. The walkthrough recommends layering both. |
| A human is already on the conversation | Stay silent. | **No Reply** with `Human Agent Assigned Equals True`. |
| Customer can't be found | Ask for the registered email, don't call the API. | The `customer_id Is Not Null` Check, wrapped in a Fallback with an asking Reply. |
| Reply quotes a date or amount the API didn't return | Rewrite or escalate. | A **Custom rule** in [Guardrails](/en/configuration/guardrails), plus Main Guidelines guardrails against fabricated commitments. |

## What to measure

Scope [Analytics](/en/analytics) to cancellations with the **Tags** filter 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 cancellation conversations the agent completed. Conversations waiting on a confirmation count as deflected, not resolved, so watch the **Waiting for Customer** share too. |
| AI Resolve Rate and Escalated Rate | **Intent rule breakdown** row for your cancellation rule | Whether the rule completes or leaks to humans. If the escalated rate climbs while the rule keeps firing, the issue is in the Reply, the Action or Reply Rules. |
| Human escalation rate | KPI card, filtered | Your team's cancellation workload, including the VIP review gate. |
| Escalation reasons | **Escalation reason** filter | **API or System Failure** for billing errors; **Escalation Constraint** for your own policy branches. |
| CSAT | **Average CSAT** and the **CSAT** filter | How customers felt about the experience of leaving, which affects whether they come back. |

If you collect reasons with a Form, the structured values give you consistent reasons to report on.

## Related

<CardGroup cols={2}>
  <Card title="Cancellation flow walkthrough" icon="route" href="/en/walkthroughs/cancellation-flow">
    The step-by-step build: attribute, Action, Intent Rule, Reply Rules gate, testing and troubleshooting.
  </Card>

  <Card title="Order status and changes" icon="boxes-packing" href="/en/walkthroughs/order-status-and-changes">
    A Fallback-rooted rule handling several related intents, the shape for pause and downgrade branches.
  </Card>

  <Card title="Tags" icon="tag" href="/en/configuration/tags">
    Intent tags, confirmation groups and writing "when not to apply" rules.
  </Card>

  <Card title="Intent Rules" icon="diagram-project" href="/en/automations/rulebook">
    Node types, inactivity follow-ups and the Form pattern.
  </Card>

  <Card title="Refunds and returns" icon="money-bill-transfer" href="/en/use-cases/refunds">
    When the customer wants money back rather than to stop the subscription.
  </Card>

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

For a comparison of tools for subscription cancellation, see the guide on [usefini.com](https://www.usefini.com/guides/top-ai-tools-subscription-cancellation).


## Related topics

- [End-to-end: cancellation flow](/en/walkthroughs/cancellation-flow.md)
- [MCP Connections](/en/api-reference/connect-mcp.md)
- [Inbox](/en/testing/inbox.md)


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