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
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
directionfor invoices, so reminders go to the customer and payments go to the vendor. - Only pass
last4for 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
remindorforward, draft the message and confirm recipients before sending. Ondispute, use the user's reason.
Three kinds of document
kind | For | Key facts shown | Default actions |
|---|---|---|---|
bill | Something the user owes: a utility, a subscription, rent | Due date, billing period | Pay, Download |
invoice | Sent by the user, or received from a vendor | Due date | Download. Send reminder first when direction is sent and it's overdue; Pay first when received and owed |
receipt | Proof of a purchase | Purchased, return-by date | Download, 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
onResultonce.
Installation
npx shadcn@latest add https://your-site.com/r/invoice.json
Usage
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:
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
| Prop | Type | Notes |
|---|---|---|
kind | "invoice" | "bill" | "receipt" | |
number | string? | 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 |
issuedAt | string | ISO 8601 date. Shown as "Purchased" on receipts; in Details for bills and invoices. Data-bound |
dueAt | string? | Bills and invoices. Data-bound |
paidAt | string? | Data-bound |
state | "draft" | "open" | "due-soon" | "overdue" | "paid" | "partially-paid" | "void" | "refunded" | As the source reports it. Data-bound |
currency | string | ISO 4217 code for every amount |
lines | { description, quantity?, unitPrice?, amount, note? }[] | 1–100, as printed. Data-bound |
subtotal | number? | Data-bound |
discounts | { label, amount }[]? | Positive amounts, shown as deductions. Data-bound |
tax | { label, amount, included? }[]? | included for VAT-inclusive prices. Data-bound |
shipping | number? | Data-bound |
total | number | Data-bound |
amountPaid | number? | Data-bound |
amountDue | number? | 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 |
returnBy | string? | 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 |
actions | InvoiceAction[]? | Up to 6; the first is primary. Defaults by kind and state |
Host props: status (streaming, ready, submitting, done), now, onResult, result and className.