> ## 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 refunds and returns

> How Fini looks up the order, checks refund or return eligibility against your policy, submits the refund through your API, and hands exceptions to your team with the full trace.

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 refunds and returns end to end inside a deterministic Intent Rule: it identifies the customer, looks up the order or charge in your system through an Action, checks eligibility against your policy, submits the refund or creates the return through a second Action, and replies with the real amount, reference and timeline your API returned. Anything outside policy, above the amount you allow the agent to approve, or marked as sensitive goes to your team as an internal note or an escalation with the conversation history, and the AI Steps trace in the Inbox shows every step the rule ran.

The agent never decides on its own whether a refund is allowed. Your API and the Checks in your rule make that decision, the agent collects what's needed and explains the outcome. Policy questions that aren't requests (*"what's your return window?"*) are answered from your Knowledge articles without running the rule at all.

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

    C->>F: Asks for a refund on an order
    Note over F: Intent Check, customer Check, Read order_id and reason
    F->>API: Check Refund Eligibility
    API-->>F: eligible, refund_amount, blocker_reason
    alt Eligible and within your auto-approve limit
        F->>API: Submit Refund with an idempotency key
        API-->>F: refund_id, refund_eta_days
        F->>C: Confirms the amount, reference and timeline
    else Eligible, above the limit
        F->>C: The request is with the team, who will follow up
        F->>T: Escalated, with the conversation history
    else Not eligible
        F->>C: Explains blocker_reason and links the refund policy
    end
```

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

| Fini handles | Your team handles |
| - | - |
| Refund and return policy questions, answered from your Knowledge articles. | Exceptions to policy and goodwill refunds outside your rules. |
| Identifying the customer and the order or charge (User Attributes, plus a Read for the order id). | Refunds above the auto-approve limit you set in the rule. |
| Checking eligibility by calling your eligibility logic through an Action. | Refunds you've routed to review with Reply Rules (for example, Enterprise customers). |
| Submitting the refund, or creating a return and returning the label or instructions, through an Action. | Cases where the customer disputes a charge they don't recognize (see [Disputes and chargebacks](/en/use-cases/disputes)). |
| Confirming the refund amount, reference and expected timeline, using the values your API returned. | Anything your Planning Prompt's Escalation Topics send to a human, such as legal threats. |
| Explaining why a request isn't eligible, using the reason your API returned. | Conversations where an Action failed and the agent escalated. |

## How it works in Fini

A refund flow is the same four-piece shape as the [Cancellation flow walkthrough](/en/walkthroughs/cancellation-flow): a User Attribute for context, Actions for the work, an Intent Rule for orchestration, and Reply Rules for overrides. Returns add a branch, which is the multi-intent pattern from the [Order status and changes walkthrough](/en/walkthroughs/order-status-and-changes).

| Building block | What it does for refunds and returns | Where |
| - | - | - |
| **Tags** | A Rulebook-enabled tag group with tags such as `refund_request` and `return_request`. The example uses a custom `Topic` group: turn on **Tag Group available in Rulebooks**, set **Tag Selection** to "Exactly one tag is selected", and assign the group to your agent. (Default groups such as **Type of Issue** have their tag list and Rulebook availability locked.) Use each tag's "when not to apply" section to separate a refund *request* from a refund *policy question*. | [Tags](/en/configuration/tags) |
| **User Attributes** | A `Customer Identity` attribute that exposes `customer_id`, `plan` and any flags you route on (for example `is_vip`). Turn on **Use in Rulebooks** for fields Checks and Reply Rules read; keep internal ids off **Visible to AI**. | [Attributes](/en/api-reference/attributes) |
| **Actions** | A read-only `Check Refund Eligibility` (or `Lookup Order`) Action, then a `Submit Refund` or `Create Return` Action. Each declares typed inputs and outputs, so Checks downstream can compare amounts with numeric operators. | [Actions](/en/api-reference/actions) |
| **Intent Rule** | The behavior tree: an intent Check, a customer Check, a Read for the order id and reason, the eligibility Tool, then a Fallback that either submits the refund or explains why not. | [Intent Rules](/en/automations/rulebook) |
| **Reply Rules** | Optional overrides. The documented pattern is **Internal Comment** with `Type of Issue Equals refunds`, so the agent drafts the reply for your team instead of sending it. Scope it with a User Attribute (for example `plan Equals enterprise`) if you only want review for some customers. | [Reply Rules](/en/automations/reply-behavior) |
| **Knowledge** | Articles for the refund and return policy, timelines, and the fall-through paths (order not found, outside the return window). These answer policy questions and back up every path where the rule stops. | [Knowledge](/en/knowledge/overview) |
| **Guardrails** | Reply checks before delivery, for example a **Custom rule** that fails replies promising a refund the rule didn't confirm, or a **URL allowlist** limited to your own and your carrier's domains for return labels. | [Guardrails](/en/configuration/guardrails) |

### Example behavior tree

The tree below is an example to adapt, not a template that ships with Fini. Rename tags, fields and Actions to match your system, and set the auto-approve limit to your own policy (`100` is a placeholder).

```text theme={null}
Steps  (root)
├── Check: Topic Equals refund_request
├── Check: customer_id Is Not Null
├── Read:  extract `order_id` (String) and `reason` (String)
├── Tool:  Check Refund Eligibility            (read-only)
│     in:  customer_id, order_id
│     out: eligible, refund_amount, blocker_reason
└── Fallback
    ├── Steps  (auto-approve path)
    │   ├── Check: eligible Equals True
    │   ├── Check: refund_amount Less Than or Equal To 100
    │   ├── Tool:  Submit Refund
    │   │     in:  order_id, refund_amount, reason
    │   │     out: refund_id, refund_eta_days
    │   └── Reply: confirm refund_amount, refund_id and refund_eta_days
    ├── Steps  (eligible, above the auto-approve limit)
    │   ├── Check: eligible Equals True
    │   └── Reply: tell the customer the request is with the team,
    │              who will follow up directly
    └── Reply: explain blocker_reason and link the refund policy article
```

<TreeWalker
  title="Try it: run the example refund tree"
  nodes={[
{ type: "Check", label: "Topic Equals refund_request", onFail: "the rule does nothing and the agent answers from your Knowledge articles" },
{ type: "Check", label: "customer_id Is Not Null", onFail: "the rule stops cleanly because the customer couldn't be identified" },
{ type: "Read", label: "extract order_id and reason" },
{ type: "Tool", label: "Check Refund Eligibility (read-only)" },
{ type: "Check", label: "Auto-approve branch: eligible Equals True", onFail: "the above-limit branch also needs eligible Equals True, so the Fallback reaches the last Reply" },
{ type: "Check", label: "Auto-approve branch: refund_amount Less Than or Equal To 100", onFail: "the Fallback moves to the above-limit branch, whose handoff Reply escalates to your team" },
{ type: "Tool", label: "Submit Refund" },
{ type: "Reply", label: "confirm refund_amount, refund_id and refund_eta_days" }
]}
  scenarios={[
{ name: "Eligible, within the limit", outcome: "Submit Refund runs and the reply confirms the refund amount, refund_id and refund_eta_days your API returned." },
{ name: "Eligible, above the limit", stopAt: 5, escalates: true, outcome: "Nothing is submitted. The above-limit branch ends in a handoff Reply that tells the customer the request is with the team, who will follow up, and Conversation Status records Escalated to Human Agent." },
{ name: "Not eligible", stopAt: 4, outcome: "Nothing is submitted. The last Reply explains the blocker_reason your API returned and links the refund policy article." },
{ name: "Policy question, not a request", stopAt: 0, outcome: "The rule doesn't run its steps. The agent answers the question from your refund policy article." }
]}
  note="Example tree to adapt, not a default configuration. Steps inside the Fallback are labelled with their branch; 100 is a placeholder limit."
/>

How it runs:

1. The intent Check is a deterministic backstop on the planner's routing. If the conversation isn't tagged `refund_request`, the Steps short-circuits, the rule does nothing on this message, and the agent falls through to its default reply from Knowledge.
2. The customer Check stops the rule cleanly if the customer couldn't be identified. To ask for the account email instead, wrap the rule in a Fallback whose second child is a Reply, the graceful-recovery pattern in [Intent Rules](/en/automations/rulebook#a-worked-example-cancellation-flow).
3. The eligibility Tool runs before anything with side effects. Its outputs sit outside the inner Fallback, so every branch below can use `blocker_reason` and `refund_amount`.
4. The Fallback tries the auto-approve path first. If the refund is eligible but above your limit, the second branch escalates through its handoff **Reply**, which tells the customer a teammate will follow up. [Conversation Status](/en/configuration/tags#mandatory-groups-conversation-status) records the conversation as **Escalated to Human Agent**. If the refund isn't eligible, the last Reply explains why using the reason your API returned.

For returns, make the root a Fallback with one Steps branch per intent: the refund branch above, and a return branch with its own intent Check (`Topic Equals return_request`) that calls a `Create Return` Action and replies with the return label or drop-off instructions it returns. One rule with a Fallback root handling several related intents is the shape the [Order status and changes walkthrough](/en/walkthroughs/order-status-and-changes) builds step by step.

<Tip>
  **Make the submit Action idempotent.** A customer can send the same message twice before the first reply arrives, and each message re-runs the rule from the root. Pass a deterministic idempotency key in the `Submit Refund` body (for example `${order_id}-refund`) so a duplicate Tool call doesn't refund twice.
</Tip>

## What you need

| What | Why | Where |
| - | - | - |
| **A way to identify the customer** | The rule needs `customer_id` before it calls anything. Use a signed JWT in the widget, UI metadata, or the connected helpdesk's user identifier as the attribute **Source**. | [Widget](/en/deploy/widget), [Attributes](/en/api-reference/attributes) |
| **An eligibility endpoint** | Read-only. Returns whether the order is refundable or returnable, the amount, and a human-readable reason when it isn't. Keep the policy logic in your system so it has one source of truth. | Your order, billing or payments system |
| **A refund or return endpoint** | The write call. Returns a reference id and timeline (and a label URL for returns). Should accept an idempotency key. | Your order, billing or payments system |
| **API credentials scoped to these endpoints** | Credentials go in the Data Step **Headers** JSON (for example an `Authorization` header). Scope the key to refund and lookup operations only. Endpoints you build should follow the [API contract](/en/api-reference/api-contract). | [Actions](/en/api-reference/actions) |
| **A Rulebook-enabled tag group** | The intent Check and any Reply Rules read it. | [Tags](/en/configuration/tags) |
| **Policy articles** | Answer policy questions and back up the paths where the rule stops. | [Knowledge](/en/knowledge/overview) |
| **A sandbox for testing** | Test Suite runs use recorded or mock Action responses and never call your systems. For a live end-to-end check, point the Actions at a sandbox environment or run a controlled pilot. | [Test Suite](/en/testing/test-suite) |

## Guardrails and escalation

Decide up front which refunds the agent may complete on its own, and encode each boundary in the layer built for it.

| Situation | Recommended behavior | How to configure it |
| - | - | - |
| Refund above your auto-approve limit | Don't submit; tell the customer the team will follow up. | A numeric Check on the eligibility output inside the rule, as in the example tree. |
| High-value or Enterprise customers | Run the rule, but post the reply as an internal note for review. | **Internal Comment** card in [Reply Rules](/en/automations/reply-behavior) with `plan Equals enterprise` (or `is_vip Equals True`) AND your refund tag. Note that the Tools in the rule still run. |
| Customer doesn't recognize the charge | Route to your dispute or fraud process, not the refund rule. | Add the pattern to the Planning Prompt's *Escalation Topics*, or a separate dispute rule. See [Prompts](/en/configuration/prompts). |
| Refund policy is changing | Escalate instead of quoting the old policy. | A time-bound instruction in **Incident Info** in Main Guidelines. |
| A human is already on the ticket | The agent stays silent. | **No Reply** with `Human Agent Assigned Equals True`. |
| The reply promises something the rule didn't confirm | Rewrite, or escalate if the rewrite still fails. | A **Custom rule** in [Guardrails](/en/configuration/guardrails), plus a "no fabricated commitments" rule in Main Guidelines **Guardrails**. |

<Note>
  **Reply Rules gate the reply, not the Tools.** When an Internal Comment rule matches, the rule still runs and `Submit Refund` still fires; only the customer-facing confirmation is held. If a human must approve before money moves, stop the tree before the submit Tool (as the above-limit branch does) and let your team complete the refund in your own system.
</Note>

## What to measure

Open [Analytics](/en/analytics) after a week of production traffic and scope it to this use case with the **Tags** filter (your refund and return 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**, with the Tags filter applied | How many refund conversations the agent fully resolved. Deflection also counts conversations still waiting on the customer, so read resolution first. |
| AI Resolve Rate and Escalated Rate per rule | **Intent rule breakdown** row for your refund rule | Whether the rule is leaking conversations to humans. |
| Human escalation rate | KPI card, with the filter applied | The share your team picks up. Expect a floor from the above-limit branch. |
| Escalation reasons | **Escalation reason** filter and doughnut | **API or System Failure** points at an Action; **Escalation Constraint** is your own policy at work; the **Customer requested human** reasons show where customers lost confidence. |
| CSAT | **Average CSAT** card and the **CSAT** filter | Whether customers are satisfied with the outcome, including denials. |

When a number moves, open the conversations behind it in [Inbox](/en/testing/inbox) and read the AI Steps trace. The first red dot shows where the rule stopped.

## Related

<CardGroup cols={2}>
  <Card title="Order status and changes" icon="boxes-packing" href="/en/walkthroughs/order-status-and-changes">
    Step-by-step build of a multi-intent order rule with eligibility checks, a cancel Action and blocked-path replies.
  </Card>

  <Card title="Cancellation flow" icon="route" href="/en/walkthroughs/cancellation-flow">
    The canonical intent, identify, act, confirm walkthrough that refund flows share.
  </Card>

  <Card title="Intent Rules" icon="diagram-project" href="/en/automations/rulebook">
    Node types, Fallback patterns and chaining Tools, including a refund chain.
  </Card>

  <Card title="Reply Rules" icon="turn-down-right" href="/en/automations/reply-behavior">
    Internal Comment, No Reply and Direct Reply, and how priority works.
  </Card>

  <Card title="Actions" icon="bolt" href="/en/api-reference/actions">
    Typed inputs and outputs, Data Steps and testing with Play.
  </Card>

  <Card title="Disputes and chargebacks" icon="scale-balanced" href="/en/use-cases/disputes">
    When the customer contests a charge instead of requesting a refund.
  </Card>
</CardGroup>

For a comparison of refund automation tools, see the guide on [usefini.com](https://www.usefini.com/guides/best-ai-tools-refund-automation).


## Related topics

- [Fini for billing and invoices](/en/use-cases/billing-and-invoices.md)
- [End-to-end: order status and changes](/en/walkthroughs/order-status-and-changes.md)
- [Fini FAQ](/en/faq.md)


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