Invoice

One invoice, bill or receipt the agent found, with its state, line items, totals and actions.

Experience · Shares patterns with Data Table, Result Receipt

≈ 2,370 tokens per use (tool 1,769 · guidance 358 · typical payload 243)How token costs are estimated

Example: Invoice

What do I owe this month for electricity?

Bill HP-26-0917

Harbor Power & Light

Amount due
$132.89
Due · in 5 days
Aug 29 – Sep 28, 2026
Line items
DescriptionQuantity and unit priceAmount
Basic service charge
$11.00
Energy use642 × $0.1238
$79.48
Delivery charge642 × $0.0512
$32.87
Public purpose programs
$3.21
Subtotal
$126.56
City utility tax 5%
$6.33
Total
$132.89

When to use

  • The user asks about a specific invoice, bill or receipt the agent found in their email, accounting tool or bank: whether it was paid, what they owe, when it's due, or where the receipt is.
  • They need the line items, totals or payment details of that one document, or to act on it (pay, download, add to expenses).

When not to use

  • The agent doesn't have the document from a tool result. Never invent amounts, dates or states.
  • The user wants an overview of many documents. Use a Data Table.
  • The agent just took an action (sent a reminder, paid a bill). Show a Result Receipt.
  • The user is still shopping. Use a Cart.

How the agent should use it

  • Pass every amount, date, party and state exactly as the source returned them, in one currency. Don't round, recompute or fix totals: the component flags totals that don't add up.
  • Set direction for invoices, so reminders go to the customer and payments go to the vendor.
  • Only pass last4 for payment, never a full card or account number. The schema rejects anything but 4 digits.
  • On pay, open the host's payment flow. Never collect payment details in chat, and never treat the document as paid until the source says so.
  • On remind or forward, draft the message and confirm recipients before sending. On dispute, use the user's reason.

Three kinds of document

kindForKey facts shownDefault actions
billSomething the user owes: a utility, a subscription, rentDue date, billing periodPay, Download
invoiceSent by the user, or received from a vendorDue dateDownload. Send reminder first when direction is sent and it's overdue; Pay first when received and owed
receiptProof of a purchasePurchased, return-by dateDownload, Add to expenses

Pass actions to choose the buttons yourself, from the closed set pay, download, forward, dispute, add-to-expenses and remind. The first is the primary button and the second a text button; any more show as labelled icon buttons. Pay and remind are hidden when nothing is owed (paid, void, refunded, draft).

Totals that don't add up

The component reconciles the document in integer minor units, within 0.01: line amounts against the subtotal, the subtotal less discounts plus taxes and shipping against the total, and amount paid plus amount due against the total. When a check fails it shows a short warning, such as "Totals don't add up: lines sum to $15.00, but the total is $16.00", instead of hiding it. The agent must not paper over it: pass the figures as printed and mention the discrepancy.

Taxes already inside VAT-inclusive prices take included: true; they show as "Includes VAT 18%" and aren't added again.

The helpers live in invoice-utils.ts (reconcile, amountOwed, relativeDue, defaultActions, …), pure functions you can reuse on the server.

Layout

The card leads with one figure: the amount due while something is owed, otherwise the total, with the due (or return-by) date under it. Line items, the totals breakdown and the payment method follow. The issuer (or, for invoices you sent, the recipient) is the one line under the title. A collapsed Details row holds the rest: both parties with their addresses, tax IDs and email, the issue date of bills and invoices, and the payment reference.

Dates

Due and return dates get relative text: "Due Oct 13 · in 5 days", "Due Sep 19 · Overdue by 19 days", "Return by Oct 24 · 16 days left to return". It's computed after mount, so server and client render the same markup. Pass now to compare with a fixed moment instead (tests, demos, a server-rendered snapshot). Overdue text uses destructive color plus an icon, and the state badge carries its own label, so nothing relies on color alone. The "Due soon" badge is left out when the due line already says it.

Actions

  • Pay is a handoff. The button reads "Pay $132.89" but never collects payment: it returns { action: "pay" } and your app opens its own payment flow (the biller's portal, your payments page, a native sheet).
  • Dispute opens a text box for the reason, and returns { action: "dispute", reason }.
  • After an action, its button locks and the footer confirms it quietly ("Sent to your payment flow", "Added to expenses"). The other actions stay available, and each calls onResult once.

Installation

npx shadcn@latest add https://your-site.com/r/invoice.json

Usage

app/api/chat/route.ts
import { tool } from "ai"
import { invoiceTool } from "@/components/genui/invoice.schema"

const tools = {
  // No execute: the user's first action is the tool result.
  [invoiceTool.name]: tool({
    description: invoiceTool.description,
    inputSchema: invoiceTool.inputSchema,
  }),
}

Render the tool call and handle each action:

components/message.tsx
import { Invoice } from "@/components/genui/invoice"

function ToolPart({ part, addToolResult }) {
  if (part.type !== "tool-show_invoice") return null

  return (
    <Invoice
      {...part.input}
      status={part.state === "input-streaming" ? "streaming" : undefined}
      result={part.state === "output-available" ? part.output : undefined}
      onResult={(result) => {
        if (result.action === "pay") openPaymentFlow(part.input)
        if (result.action === "download") downloadDocument(part.input)
        if (part.state !== "output-available") {
          addToolResult({ tool: "show_invoice", toolCallId: part.toolCallId, output: result })
        } else {
          // Later actions: send them as a follow-up so the agent can act.
          sendMessage({ text: `Invoice action: ${JSON.stringify(result)}` })
        }
      }}
    />
  )
}

The agent receives one of:

{ action: "pay" }
{ action: "download" }
{ action: "forward" }
{ action: "add-to-expenses" }
{ action: "remind" }
{ action: "dispute", reason: "Charged twice for September" }

With LangGraph

Push it from a graph node with ui.push({ name: "show_invoice", props: { toolCallId, ...args } }, { message }), and render the thread's UI messages with the Renderer.

Restoring from history

When a conversation is reloaded, pass the stored result back as result, or an array of results when several actions were taken. Those actions show as done and their buttons stay locked; they never call onResult again. With the AI SDK, that's the tool call's output:

<Invoice
  {...part.input}
  result={part.state === "output-available" ? part.output : undefined}
/>

Schema

PropTypeNotes
kind"invoice" | "bill" | "receipt"
numberstring?e.g. "1042" shows as "Invoice #1042". Data-bound
direction"sent" | "received"?Invoices only: sent when the user issued it
issuer{ name, address?, taxId?, email? }name is under the title; the rest is in Details. address lines separated by \n. Data-bound
recipient{ name, address?, taxId? }?name is under the title for sent invoices; everything is in Details. Data-bound
issuedAtstringISO 8601 date. Shown as "Purchased" on receipts; in Details for bills and invoices. Data-bound
dueAtstring?Bills and invoices. Data-bound
paidAtstring?Data-bound
state"draft" | "open" | "due-soon" | "overdue" | "paid" | "partially-paid" | "void" | "refunded"As the source reports it. Data-bound
currencystringISO 4217 code for every amount
lines{ description, quantity?, unitPrice?, amount, note? }[]1–100, as printed. Data-bound
subtotalnumber?Data-bound
discounts{ label, amount }[]?Positive amounts, shown as deductions. Data-bound
tax{ label, amount, included? }[]?included for VAT-inclusive prices. Data-bound
shippingnumber?Data-bound
totalnumberData-bound
amountPaidnumber?Data-bound
amountDuenumber?Defaults to total minus amount paid. Data-bound
payment{ method, last4?, reference? }?method: card, bank, cash, wallet, other. last4: exactly 4 digits. reference is in Details. Data-bound
returnBystring?Receipts. Data-bound
period{ start, end }?Bills: the service period. Data-bound
source{ label, url? }Where it was found; url http(s) only. Data-bound
actionsInvoiceAction[]?Up to 6; the first is primary. Defaults by kind and state

Host props: status (streaming, ready, submitting, done), now, onResult, result and className.

Source

On this page