Build

Treatment billing and RFAs

Embed treatment bills with expected OMFS payment and the complete Request for Authorization workflow in your application.

A treatment integration has two connected parts. Billers enter a treatment (CMS-1500 professional) bill and see the expected California OMFS payment for every line as they type. Clinicians and staff request authorization for that treatment with a DWC Form RFA, send it, and record the utilization review (UR) decision. MindBill links the two: an approved treatment can be attached to the bill line it authorizes.

Install @mindbill/react 0.79.0, which brings @mindbill/browser 0.52.0:

Terminal
npm install @mindbill/react@0.79.0

Enable the organization

Treatment billing, RFAs, and fee quotes require the treatmentBilling capability on the customer organization. MindBill enables it during onboarding. Until then, these endpoints return HTTP 403 with problem code treatment_billing_not_enabled. Show a clear "not enabled for this practice" state rather than retrying. Enabling the capability does not grant any user access: every request still needs the matching scope or browser permission.

The organization also needs saved records the workflow refers to by ID: rendering providers (with a saved signature for physicians who sign RFAs), service locations, and a billing provider. Manage them in billing settings. Live RFA delivery uses the organization's managed return fax number, which MindBill configures; sandbox never sends.

Session permissions

Your server authenticates the user and mints a short-lived, exact-origin browser session. RFA and fee-quote routes need an organization-wide session; bill-scoped sessions are rejected.

CapabilityBrowser permissionServer scope
Expected OMFS payment, related records, UR decisions on a billbills:read (+ rfas:read for related records)bills:read (+ rfas:read)
Submit the treatment billbills:createbills:write
List and resume bill drafts / save, replace, and delete thembills:read / bills:createbills:read / bills:write
Attach UR decisions to an unsent billbills:actbills:write
List, view, export RFAs; form and packet previewsrfas:read (+ documents:read for stored PDFs)rfas:read
Creation choices, new drafts, draft previewrfas:createrfas:write
Edit drafts, upload documents, close a treatmentrfas:editrfas:write
Sign; save a physician signature (+ organization:manage)rfas:signrfas:sign
Deliver, forward, record receipt and decisions, read UR responses, tasksrfas:actrfas:write
server/treatment-session.ts
// server: POST /api/mindbill/treatment-session (any framework)
// Map the signed-in user's role to the smallest set of permissions they need.
const session = await mindbill.createBrowserSession({
  subject: user.id,
  allowedOrigin: process.env.APP_ORIGIN!, // exact origin, e.g. https://app.example.com
  permissions: [
    "bills:read", "bills:create", "bills:act",          // treatment bills and UR attachments
    "rfas:read", "rfas:create", "rfas:edit",            // RFA list, drafts, documents
    ...(user.canSign ? ["rfas:sign"] : []),             // physician signature
    ...(user.canSend ? ["rfas:act"] : []),              // delivery, receipt, decisions, tasks
    "documents:read", "payers:read",                   // PDF previews, recipient directory
  ],
  expiresIn: 900,
});
return Response.json({ token: session.token, expiresAt: session.expiresAt });

See authentication for origin checks and error handling. Never send a permanent API key to the browser.

Create a treatment bill with expected OMFS payment

ConnectedTreatmentBillForm is the treatment bill form with three columns per service line: an editable Charge, the Expected (OMFS) payment, and the Difference. Every line of the visit is priced together, exactly as MindBill prices the saved bill, so multiple-procedure, bundling, add-on, NCCI, and same-day rules apply across lines. A line can instead show Bundled, Not payable, or Needs review with a short note. Totals never present a final amount while a line needs review. Visit-wide notes, such as a per-visit cap or the LC 4604.5 therapy visit count, appear once under the heading.

ConnectedTreatmentBillForm with synthetic lines: a modifier note on 99213, 97014 not payable, and visit totals.
Treatment bill service lines with Charge, Expected (OMFS), and Difference columns and a totals row
Treatment bill service lines with Charge, Expected (OMFS), and Difference columns and a totals row

Try it live: the preview below runs the published component against a mocked encounter quote. Change a code, units, modifier, or charge to reprice the visit.

ConnectedTreatmentBillFormMocked encounter quote and drafts · edit lines, save a draft
import { useState } from "react";
import { ConnectedTreatmentBillForm } from "@mindbill/react";

const address = { line1: "100 Example Way", city: "Oakland", state: "CA", postalCode: "94612" };
const initialBill = {
  externalId: "visit_demo_1", billingMode: "professional",
  patient: { firstName: "Alex", lastName: "Morgan", dateOfBirth: "1988-03-05", address },
  claim: {
    claimNumber: "WC-44871", employer: "Northstar Foods", dateOfInjury: "2026-05-30",
    claimsAdministrator: { id: "payer_demo", name: "Example Claims Administrator" },
  },
  service: { date: "2026-10-08" },
  billingProvider: { name: "Example Spine Group", taxId: "123456789", npi: "1234567890", phone: "5105550100", address },
  renderingProvider: { name: "Sam Example, D.C.", npi: "1098765432", taxonomy: "111N00000X" },
  serviceLocation: { name: "Example Office", placeOfServiceCode: "11", address },
  diagnoses: ["M54.50"],
  serviceLines: [
    { code: "99213", modifiers: ["25"], units: 1, charge: 180, serviceDate: "2026-10-08", diagnosisPointers: [1] },
    { code: "97110", units: 2, charge: 120, serviceDate: "2026-10-08", diagnosisPointers: [1] },
    { code: "97014", units: 1, charge: 40, serviceDate: "2026-10-08", diagnosisPointers: [1] },
  ],
};

// Mock of POST /partner/v2/fee-quotes/encounter with a tiny synthetic fee table (cents per unit).
const fees = { "99213": 9000, "97110": 4650, "97140": 4200, "98941": 5400 };
function quote(body) {
  const lines = body.lines.map((line) => {
    if (line.code === "97014") return { id: line.id, assessment: "not_separately_payable", findings: [], displayNotes: ["97014 is not payable under the California schedule; bill G0283 or omit."], quote: { status: "not_separately_payable", reason: "code_not_payable", provenance: [] } };
    const unit = fees[line.code];
    if (!unit) return { id: line.id, assessment: "requires_review", findings: [], displayNotes: ["Not in this demo's fee table."], quote: { status: "requires_review", reason: "fee_schedule_code_not_found", provenance: [] } };
    const amountCents = unit * (line.units || 1);
    const notes = line.code === "99213" && line.modifiers?.includes("25") ? ["Modifier 25: document a separately identifiable visit."] : [];
    return { id: line.id, assessment: "priced", findings: [], displayNotes: notes, quote: { status: "priced", amountCents, scheduleMaximumCents: amountCents, basis: "ca_physician_rbrvs", notes: [], provenance: [], ...(line.chargeCents ? { chargeAmountCents: line.chargeCents } : {}) } };
  });
  const priced = lines.filter((line) => line.assessment === "priced");
  const review = lines.filter((line) => line.assessment === "requires_review").length;
  const subtotal = priced.reduce((sum, line) => sum + line.quote.amountCents, 0);
  return {
    status: review ? "requires_review" : "priced", lines,
    totals: { submittedChargeCents: body.lines.reduce((sum, line) => sum + (line.chargeCents || 0), 0), pricedSubtotalCents: subtotal, estimatedPayableCents: review ? null : subtotal, scheduleMaximumCents: review ? null : subtotal, reviewLineCount: review },
    claimEdits: [], limitations: [], medicalLegalLineIds: [], visitNotes: ["Therapy visit 3 of 24 (LC 4604.5)"],
  };
}
// Mock of POST/PATCH /partner/v2/bill-drafts: keeps one draft in memory and bumps its revision per save.
let savedDraft = null;
function saveDraft(method, body) {
  const now = new Date().toISOString();
  savedDraft = {
    id: "billdraft_demo", externalId: body.externalId ?? savedDraft?.externalId ?? null, status: "draft",
    revision: method === "POST" ? 1 : savedDraft.revision + 1, claimId: null, patientId: null,
    bill: body.bill, encounter: body.encounter ?? savedDraft?.encounter ?? null,
    serviceLineCount: (body.bill.serviceLines || []).length, submittedBillId: null,
    createdAt: savedDraft?.createdAt ?? now, updatedAt: now, submittedAt: null,
  };
  return savedDraft;
}
async function mockApi(input, init = {}) {
  const url = new URL(String(input), location.href);
  if (url.pathname.endsWith("/partner/v2/fee-quotes/encounter")) return Response.json({ data: quote(JSON.parse(init.body)) });
  if (url.pathname.includes("/partner/v2/bill-drafts") && (init.method === "POST" || init.method === "PATCH")) {
    return Response.json({ data: saveDraft(init.method, JSON.parse(init.body)) }, { status: init.method === "POST" ? 201 : 200 });
  }
  return Response.json({ data: [] });
}

export default function App() {
  const [total, setTotal] = useState(null);
  const [draft, setDraft] = useState(null);
  return <main className="review-demo">
    <p className="notice">Expected payment for this visit: {total == null ? "needs review" : "$" + (total / 100).toFixed(2)}</p>
    {draft ? <p className="notice">Draft {draft.id} saved, revision {draft.revision}. Nothing was sent.</p> : null}
    <ConnectedTreatmentBillForm
      getSession={async () => ({ token: "demo_session" })}
      fetch={mockApi}
      initialBill={initialBill}
      encounter={{ renderingProviderId: "rp_demo", placeOfServiceId: "loc_demo", claimId: "claim_demo" }}
      onEncounterQuote={(estimate) => setTotal(estimate?.totals.estimatedPayableCents ?? null)}
      drafts
      draftExternalId="visit_demo_1"
      onDraftSaved={setDraft}
      deliveryRoutePicker="off"
      appearance={{ preset: "mindbill" }}
      onSubmit={async () => alert("The demo submits nothing.")}
    />
  </main>;
}
NewTreatmentBill.tsx
"use client";
import { ConnectedTreatmentBillForm } from "@mindbill/react";

export function NewTreatmentBill({ bill, ids }: { bill: TreatmentBillInput; ids: SavedIds }) {
  return <ConnectedTreatmentBillForm
    sessionEndpoint="/api/mindbill/treatment-session"
    initialBill={bill}                       // billingMode: "professional"
    encounter={{
      renderingProviderId: ids.renderingProviderId, // saved rendering provider
      placeOfServiceId: ids.locationId,             // saved service location
      claimId: ids.claimId,                         // optional: payer + prior therapy visits
    }}
    onEncounterQuote={(estimate) => setExpected(estimate?.totals.estimatedPayableCents ?? null)}
  />;
}

The encounter IDs are saved MindBill records, not free text:

  • renderingProviderId: from GET /rfas/creation-context (renderingProviders[].id) or the organization billing profile.
  • placeOfServiceId: a saved service location, locations[].id from GET /partner/v2/organization/billing-profile (see billing settings). Provider type, facility setting, and practice charges come from these saved records.
  • claimId (optional): the claim's claims administrator is used, and prior therapy visits count toward LC 4604.5 limits. billingProviderId is optional; the organization default applies when omitted.

Submission is unchanged: the form submits a professional bill through POST /bills. Link a line to an approved treatment with serviceLines[].rfaItemId; the server validates the relationship, and the ID alone is not evidence of approval.

Save and resume drafts

A biller often starts a treatment bill before every detail is known. From React 0.79.0, pass drafts to show a Save draft button next to Submit. It saves the bill exactly as entered, even if incomplete, plus the encounter pricing IDs, to POST /bill-drafts; later saves replace the same draft at its current revision. Saving validates nothing for submission, prices nothing as final, and sends nothing. A draft is not a bill: it does not appear in bill lists, events, or usage.

Pass draftId to resume. The form loads the draft, its saved bill replaces initialBill, and any encounter IDs you leave out come from the draft. When the biller presses Submit, the form sends the bill through POST /bills with draft: { id, revision }. MindBill reserves the draft, creates the bill, and marks the draft submitted with submittedBillId. If no bill is created, the draft is released so the biller can fix it and try again. A submitted draft cannot be edited, and opening one shows that it was already submitted.

TreatmentVisitBill.tsx
"use client";
import { ConnectedTreatmentBillForm } from "@mindbill/react";

export function TreatmentVisitBill({ visit, ids }: { visit: Visit; ids: SavedIds }) {
  return <ConnectedTreatmentBillForm
    sessionEndpoint="/api/mindbill/treatment-session" // bills:read + bills:create
    initialBill={visit.bill}
    encounter={{ renderingProviderId: ids.renderingProviderId, placeOfServiceId: ids.locationId, claimId: ids.claimId }}
    drafts                                  // shows "Save draft"
    draftId={visit.mindbillDraftId}         // omit for a new bill; set to resume a saved one
    draftExternalId={`visit_${visit.id}`}  // optional: your reference for a new draft
    onDraftSaved={(draft) => saveVisitDraftId(visit.id, draft.id)}
    onDraftError={() => clearVisitDraftId(visit.id)} // the saved draft could not be opened
    onSubmitted={(result) => openBill(result.billId)}
  />;
}
PropPurpose
draftsShow Save draft. The session needs bills:create.
draftIdResume this saved draft; submitting it marks the draft submitted.
draftExternalIdYour reference for a new draft, unique per organization. Reusing one returns external_id_owned.
onDraftSaved(draft)Called after each save. Store draft.id with your visit to resume later.
onDraftError(error)Called when draftId cannot be loaded, for example after it was deleted.

Using your own BillSubmissionForm? It accepts onSaveDraft(bill) (a secondary button that skips validation), saveDraftLabel, and draft: { id, revision }, which it forwards on submission. Call the drafts client yourself:

bill-drafts.ts
import { createBillDraftsClient } from "@mindbill/browser";

const drafts = createBillDraftsClient({ sessionEndpoint: "/api/mindbill/treatment-session" });

const { data } = await drafts.list({ claimId: "clm_example" });      // unsent drafts for a claim
const saved = await drafts.create({ bill, encounter, externalId: "visit_example" });
const updated = await drafts.update(saved.id, saved.revision, { bill: editedBill });
await drafts.remove(updated.id, updated.revision);                   // unsent drafts only

Server integrations use the same routes: list, save, read, replace, and delete. Writes need an Idempotency-Key; reads need bills:read and writes bills:write (browser bills:create).

Save, then submit a draft
# Save (201) — later saves PATCH with the current revision
curl https://app.mindbill.org/partner/v2/bill-drafts \
  --request POST \
  --header "Authorization: Bearer $MINDBILL_API_KEY" \
  --header "Idempotency-Key: visit_example-draft" \
  --header "Content-Type: application/json" \
  --data '{"externalId":"visit_example","bill":{"billingMode":"professional","claim":{"id":"clm_example"},"serviceLines":[{"code":"99213","units":1,"charge":150}]},"encounter":{"renderingProviderId":"rp_example","placeOfServiceId":"loc_example"}}'

# Submit the completed bill through the normal create-bill request
curl https://app.mindbill.org/partner/v2/bills \
  --request POST \
  --header "Authorization: Bearer $MINDBILL_API_KEY" \
  --header "Idempotency-Key: visit_example-submit" \
  --header "Content-Type: application/json" \
  --data '{"bill": { …the complete bill… }, "draft": {"id":"billdraft_example","revision":2}}'

Price a visit from your own UI

Call POST /fee-quotes/encounter from your server or with a browser session. Amounts are integer cents. A quote creates and changes nothing.

server/quote-visit.ts
const response = await fetch("https://app.mindbill.org/partner/v2/fee-quotes/encounter", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.MINDBILL_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    claimId: "clm_example",
    renderingProviderId: "rp_example",
    placeOfServiceId: "loc_example",
    dateOfService: "2026-09-18",
    lines: [
      { id: "line-1", code: "99213", units: 1, modifiers: ["25"], chargeCents: 18000 },
      { id: "line-2", code: "97110", units: 2, chargeCents: 12000 },
    ],
  }),
});
const { data } = await response.json();
for (const line of data.lines) {
  // Only priced lines have an expected amount. Never substitute the charge.
  const expected = line.assessment === "priced"
    ? (line.paymentAdjustment?.amountCents ?? line.quote.amountCents)
    : null;
  render(line.id, expected, line.displayNotes);
}
renderVisitNotes(data.visitNotes); // shown once for the visit

To keep your own BillSubmissionForm layout, pass an encounter quoter instead:

encounter-pricing.ts
import { createBillReferenceClient, createEncounterQuoter, encounterQuoteRequest } from "@mindbill/browser";

const client = createBillReferenceClient({ sessionEndpoint: "/api/mindbill/treatment-session" });
// Pass to your own <BillSubmissionForm onQuoteEncounter={...} />
const onQuoteEncounter = createEncounterQuoter(client, { renderingProviderId, placeOfServiceId, claimId });
// Or call it directly:
const estimate = await client.quoteEncounter(encounterQuoteRequest(lines, { renderingProviderId, placeOfServiceId }));

Run the full RFA workflow

RfaDashboard embeds the complete workflow: search and filter requests, create a request on the official DWC Form RFA, attach clinical PDFs, preview the form, sign, review the exact packet, send by fax or email, and then record receipt, information requests, and UR decisions per treatment. Its task board covers no-response follow-up, matching incoming responses, and posting decisions.

Authorizations.tsx
"use client";
import { RfaDashboard } from "@mindbill/react";

export function Authorizations({ user, claimId }: { user: SignedInUser; claimId?: string }) {
  return <RfaDashboard
    sessionEndpoint="/api/mindbill/treatment-session"
    officialForm                 // DWC Form RFA field order and labels
    layout="simple"              // All RFAs list + bill-style request detail
    claimId={claimId}            // optional: restrict to one claim
    actorReference={user.id}     // the authenticated human who signs or sends
    permissions={user.rfaControls} // e.g. ["create", "edit", "sign", "send", "act"]
    environment="sandbox"        // "live" only for approved organizations
  />;
}
  • officialForm lays out creation in DWC Form RFA order (request type, Employee, Requesting Physician, Claims Administrator, Requested Treatment) with the official labels. Values that come from saved records are read-only.
  • layout="simple" opens on the searchable All RFAs list and shows each request like a bill detail page: a milestone status card, RFA details, documents, review and send (or utilization review after delivery), history, and a sticky next-action bar.
  • Built-in defaults from React 0.78.0: when you do not supply your own handlers, form and draft previews open in the built-in PDF viewer (previewForm, previewDraft), UR responses are read with readResponse to prefill decisions for the user to confirm, and treatment templates load and save through the API (listTemplates, saveTemplate). Host props such as formPreviewUrl and onPreviewRequest still take precedence.
  • Nothing is signed or sent on render. Signing and delivery each need explicit confirmation by an authorized user, and permissions only controls which buttons appear; the server enforces access.

For a custom interface, createRfaClient() exposes the same operations, including previewForm(rfaId, { packet: true }), previewDraft(draft), readResponse(rfaId, documentId), listTemplates(), saveTemplate(input), and exportCsv(filters). The RFA guide covers drafts, signing, delivery evidence, review clocks, and corrections in detail.

Show authorizations on claim, RFA, and bill pages

Three components connect authorization records to the rest of your application. Each accepts the same session options, or a host loader if your backend serves the data.

RfaClaimIndex: every RFA on a claim

Treatment counts (Incomplete, No response, Approved, Modified, Denied), an RFAs tab and a Requested treatments tab, sortable columns, and an Add RFA action. Unsent requests count once as Incomplete; a treatment closed as no longer needed shows "Not required".

RfaClaimIndex, RFAs tab (synthetic claim).
RfaClaimIndex showing status counts and three requests on one claim
RfaClaimIndex showing status counts and three requests on one claim
Requested treatments tab: one row per treatment with code, decision, and authorization number.
RfaClaimIndex Requested treatments tab listing each treatment with its decision
RfaClaimIndex Requested treatments tab listing each treatment with its decision
RfaClaimIndexMocked RFA list · synthetic claim
import { RfaClaimIndex } from "@mindbill/react";

const item = (id, service, code, outcome, extra = {}) => ({
  id, diagnosisCode: "M54.50", serviceDescription: service, procedureCode: code,
  frequency: "2x/week", duration: "4 weeks", outcome, quantity: 8, units: null,
  authorizationNumber: outcome === "approved" || outcome === "modified" ? "DEMO-AUTH-" + id : null,
  decisionReason: null, decidedAt: outcome === "pending" ? null : "2026-09-10T17:00:00Z", ...extra,
});
const rfa = (id, status, lifecycleStatus, provider, created, submitted, items, extra = {}) => ({
  id, status, lifecycleStatus, renderingProviderId: "rp_" + provider.length, providerName: provider,
  expedited: false, requestType: "initial", contentRevision: 1, claimId: "claim_demo", patientId: "patient_demo",
  claimsAdminId: null, employeeName: "Alex Morgan", claimNumber: "WC-44871", reviewType: "prospective",
  createdAt: created, updatedAt: created, signedAt: submitted, submittedAt: submitted, receivedAt: null,
  decisionDueAt: submitted ? "2026-09-20T00:00:00Z" : null, decisionDeadlineBasis: null,
  incompleteReason: null, deferredReason: null, closedReason: null,
  readiness: { ready: true, missing: [] }, items, documents: [], ...extra,
});
const rfas = [
  rfa("rfa_5f2c91aa01", "approved", "closed", "Morgan Chen, MD", "2026-08-20T10:00:00Z", "2026-08-21T10:00:00Z", [
    item("a1", "Physical therapy, lumbar spine", "97110", "approved"),
    item("a2", "Lumbar MRI without contrast", "72148", "denied"),
  ]),
  rfa("rfa_7b13e0cc02", "submitted", "sent", "Riley Park, DO", "2026-09-12T10:00:00Z", "2026-09-13T10:00:00Z", [
    item("b1", "Chiropractic manipulative treatment, spinal, 3-4 regions", "98941", "pending"),
  ], { expedited: true }),
  rfa("rfa_9d44a1ee03", "draft", "incomplete", "Morgan Chen, MD", "2026-09-15T10:00:00Z", null, [
    item("c1", "Acupuncture, 1 or more needles", "97810", "pending"),
  ]),
];
const related = { claimId: "claim_demo", hasMore: false, data: [
  { id: "bill_1042", label: "Bill #1042", treatments: ["Physical therapy, lumbar spine"], relationship: "Linked authorization", status: "submitted", serviceDate: "2026-09-12" },
  { id: "bill_1031", label: "Bill #1031", treatments: ["Office visit, established patient"], relationship: "Same injury", status: "paid", serviceDate: "2026-08-30" },
] };
const decisions = { editable: true,
  linked: [
    { rfaId: "rfa_5f2c91aa01", rfaItemId: "a1", service: "Physical therapy, lumbar spine", form: { state: "attached" }, decision: { state: "attached", authorizationNumber: "DEMO-AUTH-a1" } },
  ],
  decisions: [
    { documentId: "doc_1", rfaId: "rfa_5f2c91aa01", filename: "ur-decision.pdf", decidedAt: "2026-09-10T17:00:00Z", attached: false, matchesBill: true, treatments: [
      { service: "Physical therapy, lumbar spine", code: "97110", outcome: "approved", authorizationNumber: "DEMO-AUTH-a1", effectiveFrom: "2026-09-10", effectiveTo: "2026-10-10" },
      { service: "Lumbar MRI without contrast", code: "72148", outcome: "denied", authorizationNumber: null, effectiveFrom: null, effectiveTo: null },
    ] },
    { documentId: "doc_2", rfaId: "rfa_7b13e0cc02", filename: "ur-decision-2.pdf", decidedAt: null, attached: true, matchesBill: false, treatments: [
      { service: "Office visit", code: "99213", outcome: "modified", authorizationNumber: "DEMO-AUTH-x", effectiveFrom: "2026-09-01", effectiveTo: "2026-09-30" },
    ] },
  ] };

// Mock of the partner API routes these components call through a browser session.
async function mockApi(input, init = {}) {
  const path = new URL(String(input), location.href).pathname;
  if ((init.method || "GET") !== "GET") return Response.json({ data: { attached: 1 } });
  if (path.endsWith("/rfas/related")) return Response.json(related);
  if (path.endsWith("/ur-decisions")) return Response.json({ data: decisions });
  if (path.endsWith("/rfas")) return Response.json({ data: rfas, nextCursor: null, summary: { total: rfas.length, byStatus: {} } });
  return Response.json({ error: { message: "Not in this demo: " + path } }, { status: 404 });
}
const session = { getSession: async () => ({ token: "demo_session" }), fetch: mockApi, appearance: { preset: "mindbill" } };

export default function App() {
  return <main className="review-demo">
    <RfaClaimIndex
      {...session}
      claimId="claim_demo"
      canCreate
      onCreateRfa={() => alert("Open your RFA creation page")}
      onSelectRfa={(rfaId) => alert("Open " + rfaId)}
    />
  </main>;
}

RelatedAuthorizationRecords: bills and RFAs on the same claim

On an RFA page (rfaId) it lists bills; on a bill page (billId) it lists RFAs. Records where a bill line uses one of the request's approved treatments ("Linked authorization") come first; other records for the injury are folded below.

RelatedAuthorizationRecords on an RFA page.
RelatedAuthorizationRecords listing a linked bill and another bill for the same injury
RelatedAuthorizationRecords listing a linked bill and another bill for the same injury
RelatedAuthorizationRecordsMocked related records · synthetic claim
import { RelatedAuthorizationRecords } from "@mindbill/react";

const item = (id, service, code, outcome, extra = {}) => ({
  id, diagnosisCode: "M54.50", serviceDescription: service, procedureCode: code,
  frequency: "2x/week", duration: "4 weeks", outcome, quantity: 8, units: null,
  authorizationNumber: outcome === "approved" || outcome === "modified" ? "DEMO-AUTH-" + id : null,
  decisionReason: null, decidedAt: outcome === "pending" ? null : "2026-09-10T17:00:00Z", ...extra,
});
const rfa = (id, status, lifecycleStatus, provider, created, submitted, items, extra = {}) => ({
  id, status, lifecycleStatus, renderingProviderId: "rp_" + provider.length, providerName: provider,
  expedited: false, requestType: "initial", contentRevision: 1, claimId: "claim_demo", patientId: "patient_demo",
  claimsAdminId: null, employeeName: "Alex Morgan", claimNumber: "WC-44871", reviewType: "prospective",
  createdAt: created, updatedAt: created, signedAt: submitted, submittedAt: submitted, receivedAt: null,
  decisionDueAt: submitted ? "2026-09-20T00:00:00Z" : null, decisionDeadlineBasis: null,
  incompleteReason: null, deferredReason: null, closedReason: null,
  readiness: { ready: true, missing: [] }, items, documents: [], ...extra,
});
const rfas = [
  rfa("rfa_5f2c91aa01", "approved", "closed", "Morgan Chen, MD", "2026-08-20T10:00:00Z", "2026-08-21T10:00:00Z", [
    item("a1", "Physical therapy, lumbar spine", "97110", "approved"),
    item("a2", "Lumbar MRI without contrast", "72148", "denied"),
  ]),
  rfa("rfa_7b13e0cc02", "submitted", "sent", "Riley Park, DO", "2026-09-12T10:00:00Z", "2026-09-13T10:00:00Z", [
    item("b1", "Chiropractic manipulative treatment, spinal, 3-4 regions", "98941", "pending"),
  ], { expedited: true }),
  rfa("rfa_9d44a1ee03", "draft", "incomplete", "Morgan Chen, MD", "2026-09-15T10:00:00Z", null, [
    item("c1", "Acupuncture, 1 or more needles", "97810", "pending"),
  ]),
];
const related = { claimId: "claim_demo", hasMore: false, data: [
  { id: "bill_1042", label: "Bill #1042", treatments: ["Physical therapy, lumbar spine"], relationship: "Linked authorization", status: "submitted", serviceDate: "2026-09-12" },
  { id: "bill_1031", label: "Bill #1031", treatments: ["Office visit, established patient"], relationship: "Same injury", status: "paid", serviceDate: "2026-08-30" },
] };
const decisions = { editable: true,
  linked: [
    { rfaId: "rfa_5f2c91aa01", rfaItemId: "a1", service: "Physical therapy, lumbar spine", form: { state: "attached" }, decision: { state: "attached", authorizationNumber: "DEMO-AUTH-a1" } },
  ],
  decisions: [
    { documentId: "doc_1", rfaId: "rfa_5f2c91aa01", filename: "ur-decision.pdf", decidedAt: "2026-09-10T17:00:00Z", attached: false, matchesBill: true, treatments: [
      { service: "Physical therapy, lumbar spine", code: "97110", outcome: "approved", authorizationNumber: "DEMO-AUTH-a1", effectiveFrom: "2026-09-10", effectiveTo: "2026-10-10" },
      { service: "Lumbar MRI without contrast", code: "72148", outcome: "denied", authorizationNumber: null, effectiveFrom: null, effectiveTo: null },
    ] },
    { documentId: "doc_2", rfaId: "rfa_7b13e0cc02", filename: "ur-decision-2.pdf", decidedAt: null, attached: true, matchesBill: false, treatments: [
      { service: "Office visit", code: "99213", outcome: "modified", authorizationNumber: "DEMO-AUTH-x", effectiveFrom: "2026-09-01", effectiveTo: "2026-09-30" },
    ] },
  ] };

// Mock of the partner API routes these components call through a browser session.
async function mockApi(input, init = {}) {
  const path = new URL(String(input), location.href).pathname;
  if ((init.method || "GET") !== "GET") return Response.json({ data: { attached: 1 } });
  if (path.endsWith("/rfas/related")) return Response.json(related);
  if (path.endsWith("/ur-decisions")) return Response.json({ data: decisions });
  if (path.endsWith("/rfas")) return Response.json({ data: rfas, nextCursor: null, summary: { total: rfas.length, byStatus: {} } });
  return Response.json({ error: { message: "Not in this demo: " + path } }, { status: 404 });
}
const session = { getSession: async () => ({ token: "demo_session" }), fetch: mockApi, appearance: { preset: "mindbill" } };

export default function App() {
  return <main className="review-demo">
    <RelatedAuthorizationRecords
      {...session}
      rfaId="rfa_5f2c91aa01"
      onSelectBill={(billId) => alert("Open " + billId)}
      getClaimHref={(claimId) => "#" + claimId}
    />
  </main>;
}

BillUrDecisions: authorizations for a bill

Shows the signed RFA and UR decision for each authorization linked to a service line, then every other UR decision on the bill's patient and claim. While the bill is unsent and canEdit is set, a biller can attach selected decision letters or retry the linked attachments. Attaching copies documents onto the bill; nothing is sent.

BillUrDecisions on an unsent treatment bill.
BillUrDecisions with linked authorizations and a table of UR decisions
BillUrDecisions with linked authorizations and a table of UR decisions
BillUrDecisionsMocked UR decisions · nothing is sent
import { useState } from "react";
import { BillUrDecisions } from "@mindbill/react";

const item = (id, service, code, outcome, extra = {}) => ({
  id, diagnosisCode: "M54.50", serviceDescription: service, procedureCode: code,
  frequency: "2x/week", duration: "4 weeks", outcome, quantity: 8, units: null,
  authorizationNumber: outcome === "approved" || outcome === "modified" ? "DEMO-AUTH-" + id : null,
  decisionReason: null, decidedAt: outcome === "pending" ? null : "2026-09-10T17:00:00Z", ...extra,
});
const rfa = (id, status, lifecycleStatus, provider, created, submitted, items, extra = {}) => ({
  id, status, lifecycleStatus, renderingProviderId: "rp_" + provider.length, providerName: provider,
  expedited: false, requestType: "initial", contentRevision: 1, claimId: "claim_demo", patientId: "patient_demo",
  claimsAdminId: null, employeeName: "Alex Morgan", claimNumber: "WC-44871", reviewType: "prospective",
  createdAt: created, updatedAt: created, signedAt: submitted, submittedAt: submitted, receivedAt: null,
  decisionDueAt: submitted ? "2026-09-20T00:00:00Z" : null, decisionDeadlineBasis: null,
  incompleteReason: null, deferredReason: null, closedReason: null,
  readiness: { ready: true, missing: [] }, items, documents: [], ...extra,
});
const rfas = [
  rfa("rfa_5f2c91aa01", "approved", "closed", "Morgan Chen, MD", "2026-08-20T10:00:00Z", "2026-08-21T10:00:00Z", [
    item("a1", "Physical therapy, lumbar spine", "97110", "approved"),
    item("a2", "Lumbar MRI without contrast", "72148", "denied"),
  ]),
  rfa("rfa_7b13e0cc02", "submitted", "sent", "Riley Park, DO", "2026-09-12T10:00:00Z", "2026-09-13T10:00:00Z", [
    item("b1", "Chiropractic manipulative treatment, spinal, 3-4 regions", "98941", "pending"),
  ], { expedited: true }),
  rfa("rfa_9d44a1ee03", "draft", "incomplete", "Morgan Chen, MD", "2026-09-15T10:00:00Z", null, [
    item("c1", "Acupuncture, 1 or more needles", "97810", "pending"),
  ]),
];
const related = { claimId: "claim_demo", hasMore: false, data: [
  { id: "bill_1042", label: "Bill #1042", treatments: ["Physical therapy, lumbar spine"], relationship: "Linked authorization", status: "submitted", serviceDate: "2026-09-12" },
  { id: "bill_1031", label: "Bill #1031", treatments: ["Office visit, established patient"], relationship: "Same injury", status: "paid", serviceDate: "2026-08-30" },
] };
const decisions = { editable: true,
  linked: [
    { rfaId: "rfa_5f2c91aa01", rfaItemId: "a1", service: "Physical therapy, lumbar spine", form: { state: "attached" }, decision: { state: "attached", authorizationNumber: "DEMO-AUTH-a1" } },
  ],
  decisions: [
    { documentId: "doc_1", rfaId: "rfa_5f2c91aa01", filename: "ur-decision.pdf", decidedAt: "2026-09-10T17:00:00Z", attached: false, matchesBill: true, treatments: [
      { service: "Physical therapy, lumbar spine", code: "97110", outcome: "approved", authorizationNumber: "DEMO-AUTH-a1", effectiveFrom: "2026-09-10", effectiveTo: "2026-10-10" },
      { service: "Lumbar MRI without contrast", code: "72148", outcome: "denied", authorizationNumber: null, effectiveFrom: null, effectiveTo: null },
    ] },
    { documentId: "doc_2", rfaId: "rfa_7b13e0cc02", filename: "ur-decision-2.pdf", decidedAt: null, attached: true, matchesBill: false, treatments: [
      { service: "Office visit", code: "99213", outcome: "modified", authorizationNumber: "DEMO-AUTH-x", effectiveFrom: "2026-09-01", effectiveTo: "2026-09-30" },
    ] },
  ] };

// Mock of the partner API routes these components call through a browser session.
async function mockApi(input, init = {}) {
  const path = new URL(String(input), location.href).pathname;
  if ((init.method || "GET") !== "GET") return Response.json({ data: { attached: 1 } });
  if (path.endsWith("/rfas/related")) return Response.json(related);
  if (path.endsWith("/ur-decisions")) return Response.json({ data: decisions });
  if (path.endsWith("/rfas")) return Response.json({ data: rfas, nextCursor: null, summary: { total: rfas.length, byStatus: {} } });
  return Response.json({ error: { message: "Not in this demo: " + path } }, { status: 404 });
}
const session = { getSession: async () => ({ token: "demo_session" }), fetch: mockApi, appearance: { preset: "mindbill" } };

export default function App() {
  const [message, setMessage] = useState("Select a decision letter to attach to this unsent bill.");
  return <main className="review-demo">
    <p className="notice">{message}</p>
    <BillUrDecisions
      {...session}
      billId="bill_1042"
      canEdit
      openDocument={(decision) => setMessage("Preview " + decision.filename)}
      onSelectRfa={(rfaId) => setMessage("Open " + rfaId)}
      onAttached={(result) => setMessage("Attached " + result.attached + " document. Nothing was sent.")}
    />
  </main>;
}
Claim, RFA, and bill pages
"use client";
import { BillUrDecisions, RelatedAuthorizationRecords, RfaClaimIndex } from "@mindbill/react";

const session = { sessionEndpoint: "/api/mindbill/treatment-session" };

// Claim page: every RFA on the claim, by request or by requested treatment.
<RfaClaimIndex {...session} claimId={claimId} canCreate
  createHref={`/rfas/new?claimId=${claimId}`}
  getRfaHref={(rfaId) => `/rfas/${rfaId}`} />

// RFA page: bills that use this authorization, then other bills for the injury.
<RelatedAuthorizationRecords {...session} rfaId={rfaId} getBillHref={(billId) => `/bills/${billId}`} />

// Bill page: signed RFAs and UR decisions; attach letters while the bill is unsent.
<BillUrDecisions {...session} billId={billId} canEdit={bill.unsent}
  getRfaHref={(rfaId) => `/rfas/${rfaId}`} onAttached={() => refreshDocuments()} />
records-client.ts
import { createRfaRecordsClient } from "@mindbill/browser";

const records = createRfaRecordsClient({ sessionEndpoint: "/api/mindbill/treatment-session" });
const rfas = await records.listClaimRfas("clm_example");
const related = await records.listRelated({ billId: "bill_example" });
const decisions = await records.listBillUrDecisions("bill_example");
await records.attachBillUrDecisions("bill_example", { documentIds: ["rfadoc_example"] });

Keep your application in sync

Subscribe your server to the RFA events you act on and reread the request when one arrives. Useful starting points:

  • rfa.submitted and rfa.received: delivery and receipt; receipt starts the review clock.
  • rfa.decision_recorded and rfa.decision_corrected: UR outcomes per treatment; refresh the bill's UR decisions.
  • rfa.information_requested and rfa.follow_up_due: work that needs a person.
  • Bill events such as bill.accepted, bill.paid, and payment.posted for the treatment bill.

See webhook events for every type, the envelope, and signature verification.

API routes used by this workflow

StepRoutes
Saved choicesGET /rfas/creation-context, GET /organization/billing-profile
Expected paymentPOST /fee-quotes/encounter
Bill draftsGET|POST /bill-drafts, GET|PATCH|DELETE /bill-drafts/{draftId}
Treatment billPOST /bills, GET|POST /bills/{billId}/ur-decisions
Form previewsPOST /rfas/draft-preview, GET /rfas/{rfaId}/form-preview
SignaturePOST /rfas/providers/{providerId}/signature
DeliveryPOST /rfas/{rfaId}/delivery-preview, /submit, /forwards, /sent-outside
DecisionsPOST /rfas/{rfaId}/response-reading, /decisions
Records and reportingGET /rfas/related, GET /rfas/export

The RFA guide's API map lists every RFA route.