Quick start
Three steps to render the embedded bill pay surface in your product. Every step is sandbox-safe — no real money moves.
Install the React component library
Single npm package — pulls in components, styles, and TypeScript types.
npm install @rutter/billpay-reactMint a session JWT on your backend
Each end customer gets a short-lived token scoped to one entityId. Issue it from your authenticated route.
import { signJwt } from "@rutter/billpay-server";
export async function POST(req: Request) {
const user = await getCurrentUser(req);
const token = signJwt({
entityId: user.organizationId, // scopes UI to this customer
actor: { id: user.id, email: user.email },
scope: ["bills:read", "bills:write", "payments:write"],
expiresIn: "1h",
});
return Response.json({ token });
}Drop the component into your app
The token wires up auth + entity scoping. Theme via CSS variables to match your design system.
import { BillPayProvider, BillsTable } from "@rutter/billpay-react";
import "@rutter/billpay-react/styles.css";
export default function BillPayPage({ token }: { token: string }) {
return (
<BillPayProvider token={token} theme="dark" accent="#5c7e80">
<BillsTable />
</BillPayProvider>
);
}React components
All surfaces in this demo are real components in @rutter/billpay-react. Compose them, theme them, or use the full inbox in one line.
<BillPayProvider>
Auth + theming context. Wrap the rest.
props: token · theme · accent · onError
<BillsInbox>
Full bill pay page: KPIs, tabs, table, upload.
props: filter · onBillSelect
<BillsTable>
Just the table. Filter, sort, paginate.
props: status · limit · onRowClick
<PayBillDialog>
5-step pay flow modal with invoice preview.
props: billId · onComplete · open
<InvoicePreview>
Mercury-style rendered invoice pane.
props: billId · zoom · controls
<RecipientsList>
Vendor master with W-9 status.
props: search · w9Filter
<ApprovalsPanel>
Pending approvals with one-click approve.
props: policyId · onApprove
<AuditLog>
Signed event stream + CSV export.
props: since · entityId
Theming tokens overridable: --rutter-bg --rutter-surface --rutter-foreground --rutter-accent --rutter-radius + 12 more.
JWT session
Backend mints a short-lived JWT. The UI scopes itself to one customer entity. No customer data leaks across tenants — the token is the entire trust boundary.
Token claims
{
"iss": "anatomy.com",
"sub": "user_8a2c1f",
"entityId": "ent_pacific_health",
"actor": {
"id": "user_8a2c1f",
"email": "marcus@pacifichealth.com",
"role": "approver"
},
"scope": [
"bills:read",
"bills:write",
"payments:write"
],
"iat": 1717012800,
"exp": 1717016400
}Verify on the wire
import { verifyJwt } from "@rutter/billpay-server";
const claims = await verifyJwt(token, {
issuer: "anatomy.com",
audience: "rutter-billpay",
});
// claims.entityId is enforced
// across every API callDefault expiry: 1 hour
Configurable per-org. Refresh tokens are not used; mint a fresh JWT on the backend any time the UI needs one. The component library auto-refetches on 401.
TypeScript SDK
Server-side SDK powers your backend. Same auth model — pass a connection-scoped API key. Idempotent by default.
import { Rutter } from "@rutter/billpay";
const rutter = new Rutter({
apiKey: process.env.RUTTER_KEY!,
connectionId: "conn_pacific_health",
});
// List unpaid bills for an entity
const bills = await rutter.bills.list({
entityId: "ent_pacific_health",
status: ["draft", "pending_approval", "approved"],
limit: 50,
});
// Create a bill from OCR-extracted data
const bill = await rutter.bills.create({
entityId: "ent_pacific_health",
vendorId: "ven_mckesson",
amount: 38900_00, // cents
currency: "USD",
invoiceNumber: "MCK-77194",
dueDate: "2026-05-01",
glCode: "5110",
attachments: [{ url: "..." }],
});
// Schedule payment via Anatomy's bank rail
const payment = await rutter.payments.create({
billId: bill.id,
rail: "ach", // | "wire" | "check" | "rtp"
fundingAccountId: "fund_ops_payroll",
scheduledFor: "2026-05-04",
}, {
idempotencyKey: `pay_${bill.id}_${Date.now()}`,
});Other languages on the same OpenAPI spec: @rutter/billpay-py · @rutter/billpay-go · com.rutter:billpay (Java) · RutterBillPay (.NET).
Webhooks
Every state transition fires an HMAC-signed webhook. Required for closing the loop with Anatomy's bank rail on paid / failed.
| Event | When it fires |
|---|---|
| bill.created | Bill ingested via OCR or ledger sync |
| bill.updated | Any field on a bill changes |
| bill.approval_requested | Bill enters pending approval |
| bill.approved | All required approvers signed off |
| bill.rejected | Approver declined |
| payment.scheduled | Payment queued for send |
| payment.initiated | Rail call made to bank API |
| payment.paid | Bank confirmed funds received |
| payment.failed | Rail returned an error / NSF / return |
Sample payload
{
"id": "evt_01HXYZ7K4M",
"type": "payment.paid",
"createdAt": "2026-05-05T09:24:18Z",
"data": {
"paymentId": "pay_01HXYZ7K3N",
"billId": "bill_01HXYZ7K2P",
"amount": 3890000,
"currency": "USD",
"rail": "ach",
"vendorBankReference": "JPM-2026050518-29874"
},
"signature": "v1=t1717590258,sig=8f3a..."
}Verify with rutter.webhooks.verify(rawBody, signatureHeader, secret). Replays accepted within 5 minutes; later replays rejected.
Sandbox
Same API as production. No real money moves. Test invoices and webhook fixtures included.
Sandbox base URL
https://sandbox.rutterapi.com
Test API key (per workspace)
sk_test_4eC39HqLyjWDarjtT1zdp7dc
Test fixtures
# Drive an OCR ingestion
$ curl -X POST https://sandbox.rutterapi.com/v1/billing/ingest \
-H "Authorization: Bearer sk_test_..." \
-F "file=@./fixtures/stryker-invoice.pdf"
# Force a payment failure (returns NSF on next .paid attempt)
$ curl -X POST https://sandbox.rutterapi.com/v1/sandbox/force_failure \
-H "Authorization: Bearer sk_test_..." \
-d '{"paymentId": "pay_...", "reason": "insufficient_funds"}'Anatomy bank API SDK
Anatomy's bank API isn't public-facing today (Roger flagged this on 2026-04-30, 25:39). Rutter writes a typed SDK that wraps it as part of the V1 build — net-positive deliverable.
@anatomy/bank-rails
Server-side SDK. Wraps the four payment-init endpoints + status webhooks + funding-account selection.
anatomy-bank.openapi.yaml
Generated OpenAPI 3.1 spec. Works with any code generator Anatomy wants in the future.
import { AnatomyBank } from "@anatomy/bank-rails";
const bank = new AnatomyBank({
apiKey: process.env.ANATOMY_BANK_KEY!,
environment: "production",
});
// Initiate an ACH (the rail Rutter calls when a bill ships)
const txn = await bank.payments.ach.initiate({
fromAccountId: "fund_ops_payroll", // Anatomy's funding account
amount: 3890000, // cents
currency: "USD",
recipient: {
routingNumber: "880401010",
accountNumber: "158856789",
accountName: "McKesson Pharmaceutical",
},
reference: `bill_${billId}`,
}, {
idempotencyKey: paymentInitKey,
});
// Status webhooks land back on Rutter
// → fired into the bill state machineWhat Anatomy provides for the SDK build
- POST endpoints per rail (ACH, Same-day ACH, Wire, Check)
- Payment-status webhook delivery (or polling endpoint)
- Idempotency-key support on payment-init
- Funding-account selection by ID
- Sandbox credentials