Components quickstart

Add billing to your app

Get a key, connect your backend, and drop billing components into your frontend.

Choose React or Angular below, then connect the same authenticated session endpoint to your frontend.

1. Get an API key

Create a sandbox organization in the developer console, then copy its key from API keys.

Server environment · .env.local
MINDBILL_API_KEY=your_sandbox_key\nAPP_ORIGIN=http://localhost:3000

For Next.js, create .env.local in the project root, alongside package.json, even if you use src/app. Use the exact name MINDBILL_API_KEY in both your environment and server code. Set APP_ORIGIN to your frontend’s exact origin, including its port. Restart your dev server after setting these values.

Keep the key on your server; do not add a NEXT_PUBLIC_ prefix or put it in Angular environment.ts. Use invented patient data in sandbox; sandbox submissions never reach payers.

2. Install the library

Choose your frontend. The examples below will follow your selection.

Terminal
npm install @mindbill/react@0.69.0

For Next.js, copy the examples into the files shown below, using src/app instead of app if that is your project's layout. Or run the starter app.

3. Add a session route

Your backend checks the signed-in user and uses its API key to ask MindBill for a short-lived session token. It returns that token to your frontend, which calls MindBill directly. With a backend-only integration, your server calls MindBill using the API key. The API key stays on your server in both flows.

Choose your backend below. For Next.js, copy the function into app/api/mindbill/session/route.ts. Express, FastAPI, and other backends expose the same POST /api/mindbill/session contract. Each uses the server-held MINDBILL_API_KEY from step 1 to create a short-lived browser session.

app/api/mindbill/session/route.ts
// app/api/mindbill/session/route.ts
export async function POST(request: Request) {
  const allowedOrigin = process.env.APP_ORIGIN;
  if (!allowedOrigin || request.headers.get("origin") !== allowedOrigin) {
    return Response.json({ error: "Origin not allowed" }, { status: 403 });
  }

  try {
    const access = await authorizeBillingSession(request);
    if (!access) return Response.json({ error: "Not authorized" }, { status: 403 });
    const response = await fetch(
      "https://app.mindbill.org/partner/v2/browser-sessions",
      {
        method: "POST",
        headers: {
          Authorization: `Bearer ${access.apiKey}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          subject: access.subject,
          allowedOrigin,
          permissions: access.permissions,
          ...(access.resource ? { resource: access.resource } : {}),
          expiresIn: 900,
        }),
        cache: "no-store",
        signal: AbortSignal.timeout(10_000),
      },
    );

    if (!response.ok) {
      // Log the status only, never keys, session tokens, or response bodies.
      console.error("MindBill session creation failed", response.status);
      throw new Error("Session creation failed");
    }

    return Response.json(await response.json(), {
      headers: { "Cache-Control": "no-store" },
    });
  } catch {
    return Response.json(
      { error: "Billing session unavailable" },
      { status: 503 },
    );
  }
}

// YOUR host adapter, not an SDK export. Connect existing sign-in, CSRF,
// billing roles, and server-owned customer -> MindBill credential mapping.
// Use MINDBILL_API_KEY only for a single authorized organization.
// Return null for denied access. Never accept these values from browser input.
async function authorizeBillingSession(_request: Request): Promise<{
  subject: string;
  apiKey: string;
  permissions: string[];
  resource?: { billId: string };
} | null> {
  throw new Error("Connect existing authentication and customer-key mapping first");
}

Connect authorizeBillingSession to your existing authentication before trying the page; it fails closed until then. Resolve the active customer and its server-held credential from trusted membership data. For a full billing workspace, return the scopes below. Keep sandbox and live credentials separate.

Workspace scopes: bills:create, bills:read, bills:act, documents:read, payers:read, and eors:read. Grant only the user’s permitted actions. For case-only access, resolve the saved bill server-side, set resource: { billId }, and omit bills:create. Use a separate administrator endpoint for organization:manage.

Troubleshoot “Billing session unavailable”

This message means your session route could not create a MindBill session. Confirm your host auth adapter is connected, the authorized customer has a configured key, and APP_ORIGIN exactly matches the browser origin. Save .env.local and restart your dev server after changing environment values.

If MindBill returns 401, check that you copied an active API key from the developer console. For 403, check that the key grants the permissions requested by the route. The example logs only the upstream status; keep keys, session tokens, and upstream response bodies out of logs and browser errors.

Never return an organization-wide session to a user who may access only one case. For multiple customers or access to a single bill, follow the authentication guide.

4. Add a single bill

Copy this page or component, then open /billing/new and fill out the form. Angular also needs the router setup in step 5. After submission, the component displays the bill’s status, documents, payments, actions, and shared notes with author names.

app/billing/new/page.tsx
"use client";
import { useState } from "react";
import { BillSubmissionForm, ConnectedBillLifecycle } from "@mindbill/react";

export default function NewBillPage() {
  // TODO: Load the current report from your backend.
  const report = { id: "report_demo_001", mindbillBillId: null };
  const [billId, setBillId] = useState<string | null>(report.mindbillBillId);

  function handleSubmitted({ billId }: { billId: string }) {
    setBillId(billId);
    // TODO: Save billId on this report through your backend.
    // PATCH /api/reports/:id with { mindbillBillId: billId }
  }

  if (billId) return <ConnectedBillLifecycle
    billId={billId} sessionEndpoint="/api/mindbill/session"
  />;

  return <BillSubmissionForm
    sessionEndpoint="/api/mindbill/session"
    initialBill={{
      externalId: report.id,
      patient: {
        firstName: "", lastName: "", dateOfBirth: "",
        address: { line1: "", city: "", state: "CA", postalCode: "" },
      },
      claim: { claimNumber: "" },
      service: { date: "" },
      serviceLines: [],
    }}
    onSubmitted={handleSubmitted}
  />;
}

The empty fields are editable. The sample report represents a record already in your database: use its id as externalId, and save the returned billId in its mindbillBillId field. Use a report or billable work-item ID, since a case may have several bills; no separate ID-generation endpoint is needed.

Saving that link is a good default for reopening the bill. Load the report before mounting and initialize from its saved ID; show loading errors separately. Until you connect the TODOs, the example’s selection resets on reload, but the bill remains saved in MindBill and is available from the dashboard.

Save billId in your database Optional

Once your app has authentication and a database, replace the TODO with a request to your own backend. The handler updates the screen immediately and reports a failed save without submitting the bill again.

Inside NewBillPage · replace handleSubmitted
// Replace handleSubmitted in the React example above.
async function handleSubmitted({ billId }: { billId: string }) {
  setBillId(billId); // The bill is already submitted, even if saving the link fails.
  try {
    const response = await fetch(`/api/reports/${encodeURIComponent(report.id)}`, {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      // TODO: Include your app's CSRF token if required.
      body: JSON.stringify({ mindbillBillId: billId }),
    });
    if (!response.ok) throw new Error("Could not save bill link");
  } catch {
    window.alert("Bill submitted, but saving its link failed. Recover it using externalId; do not submit again.");
  }
}

The route below illustrates a Prisma-style database write. authorizeReport and db are your app’s integrations, not MindBill exports: adapt the imports and model fields to your existing code. The auth helper must validate sign-in, billing access, and CSRF, load the authorized report, and select that report organization’s server-held MindBill token. Return null when access is denied. Keep the token on the server.

app/api/reports/[id]/route.ts · adapt to your database
// Uses YOUR auth helper and database client; adapt these imports/model names.
import { authorizeReport } from "@/lib/auth";
import { db } from "@/lib/db";

export async function PATCH(request: Request, context: {
  params: Promise<{ id: string }>;
}) {
  try {
    const { id } = await context.params;
    // Authenticates, checks billing access + CSRF, and loads the report.
    // Returns { id, organizationId, apiToken } from server-owned data, or null.
    const report = await authorizeReport(request, id);
    if (!report) return Response.json({ error: "Forbidden" }, { status: 403 });

    const body = await request.json().catch(() => null);
    const billId = body?.mindbillBillId;
    if (typeof billId !== "string" || !billId.trim()) {
      return Response.json({ error: "Bill ID required" }, { status: 400 });
    }
    // This token belongs to the report's organization, selected by your server.
    const response = await fetch(
      `https://app.mindbill.org/partner/v2/bills/${encodeURIComponent(billId)}`,
      {
        headers: { Authorization: `Bearer ${report.apiToken}` },
        cache: "no-store",
        signal: AbortSignal.timeout(10_000),
      },
    );
    if (!response.ok) throw new Error("Could not verify bill");
    const bill = await response.json();
    if (bill.id !== billId || bill.externalId !== report.id) {
      return Response.json({ error: "Bill does not match report" }, { status: 409 });
    }

    // Prisma-style example: atomically set an empty link or accept the same ID.
    const saved = await db.report.updateMany({
      where: {
        id: report.id, organizationId: report.organizationId,
        OR: [{ mindbillBillId: null }, { mindbillBillId: billId }],
      },
      data: { mindbillBillId: billId },
    });
    if (saved.count !== 1) {
      return Response.json({ error: "Report link changed; reload it" }, { status: 409 });
    }
    return Response.json({ mindbillBillId: billId }, {
      headers: { "Cache-Control": "no-store" },
    });
  } catch {
    return Response.json({ error: "Could not save bill link" }, { status: 503 });
  }
}

The server verifies both the organization and externalId before storing mindbillBillId. Repeating the save with the same ID is safe; a different existing link returns a conflict. For a failed save or a closed browser, recover the ID using the lookup below.

Find the same bill after a reload

If the report has no saved mindbillBillId, your server can recover it using the report’s existing ID as externalId.

This optional Next.js page opens the matching bill at /billing/report.

app/billing/report/page.tsx · optional
// app/billing/report/page.tsx — runs on your server.
import Link from "next/link";
import { ConnectedBillLifecycle } from "@mindbill/react";

export default async function ReportBillingPage() {
  // TODO: Authenticate the user and check access to this report.
  const externalId = "report_demo_001"; // Your saved report or work-item ID.
  const response = await fetch(
    `https://app.mindbill.org/partner/v2/bills?externalId=${encodeURIComponent(externalId)}&limit=2`,
    {
      headers: { Authorization: `Bearer ${process.env.MINDBILL_API_KEY}` },
      cache: "no-store",
    },
  );
  if (!response.ok) throw new Error("Could not look up the bill");
  const { data, nextCursor } = await response.json();
  if (data.length > 1 || nextCursor) {
    throw new Error("Multiple bills match; choose the intended bill");
  }

  return data[0] ? <ConnectedBillLifecycle
    billId={data[0].id} sessionEndpoint="/api/mindbill/session"
  /> : <Link href="/billing/new">Create a bill</Link>;
}

externalId does not enforce uniqueness or prevent duplicate submissions. Use a stable ID for each billable item. If multiple bills match, choose the intended one; a failed lookup must not be treated as “no bill.”

This also works if you choose not to store billId locally. The lookup API finds the bill even if the browser closed before your submission callback ran.

5. Add a bill dashboard

Copy this page and open /billing to find saved bills. The Add bill button opens the page from step 4.

app/billing/page.tsx
"use client";
import { ConnectedBillingWorkspace } from "@mindbill/react";

export default function BillingPage() {
  return <ConnectedBillingWorkspace
    sessionEndpoint="/api/mindbill/session"
    // Settings is included. For a separate administrator session (step 7):
    // billingSettings={{ sessionEndpoint: "/api/mindbill/settings-session" }}
    // Use showSettings={false} to hide the tab.
    onCreateBill={() => window.location.assign("/billing/new")}
  />;
}

The workspace loads bills and opens their details for you. No database or extra API route is needed.

Add more when you need it

6. Pre-fill the create-bill form Optional

In step 4, replace the empty fields in initialBill with values you already have. For example:

initialBill · edit the object in step 4
// Replace initialBill in step 4 with your known values.
{
  externalId: "report_demo_001",
  patient: {
    firstName: "Taylor", lastName: "Example", dateOfBirth: "1984-04-12",
    address: {
      line1: "100 Example Avenue", city: "Los Angeles",
      state: "CA", postalCode: "90012",
    },
  },
  claim: { claimNumber: "DEMO-12345" },
  service: { date: "2026-09-08" },
  serviceLines: [{ code: "ML201", units: 1 }],
}

For Angular, fill the matching fields in the existing initialBill object and keep its other required fields. Users can edit the values and attach PDFs before submitting. See the field reference for more options.

7. Add billing settings Optional

React 0.64.0 includes a Settings tab in the billing workspace by default. Pass billingSettings with your administrator-only settings endpoint, or let it reuse an already authorized workspace session. Set showSettings={false} to hide the tab. A separate settings page is optional; the examples below also support standalone settings and Angular.

app/billing/settings/page.tsx
"use client";
import { BillingSettings } from "@mindbill/react";

export default function BillingSettingsPage() {
  return <BillingSettings
    sessionEndpoint="/api/mindbill/settings-session"
  />;
}

Add POST /api/mindbill/settings-session to the backend from step 3, using the same session creation code with permissions ["organization:manage"]. Restrict it to administrators.

With a Next.js backend, place that handler in app/api/mindbill/settings-session/route.ts. The page above opens at /billing/settings.

React bill forms load saved provider and location choices automatically with an organization-wide billing session. Add the separate billingSettings session prop to let administrators add choices from the form. See the practice settings guide for setup, W-9 attachment, and scoped-session behavior.

For email alerts, use the administrator recipient list in React, or build an Angular settings view over the same notification API. See Angular notification settings.

8. Add RFA components Optional · treatment billing

For treatment authorization and billing, start with the prebuilt RFA dashboard and the treatment billing quickstart.

Ready for real bills? Complete the sandbox checks. For editor setup or a full implementation brief, use the integration recipes.