Build
Authentication
Call the same API with a server key or a short-lived browser session. Your application controls which customers and users can access billing.
One API, two credentials
Send Authorization: Bearer <api-key> from a trusted backend. Keep the key in your server secret store.
Your backend exchanges its key for a short-lived token. Components send that token and the browser’s exact authorized Origin.
Both use the same /partner/v2 business URLs, payloads, and responses. Each endpoint reference lists its server scopes and browser permissions. Existing /partner/v2/browser business URLs remain aliases; use the canonical URLs for new integrations.
Choose the organization and user on your server
Authenticate the user, check their billing role, and select the customer’s MindBill credential from server-owned membership data. Never accept a credential, organization, subject, or permission list from the request body.
Organization-scoped keys and browser sessions are fixed to one organization. An account-scoped partner key can use /organizations/{id} for linked organizations it may manage. For normal business routes, account-scoped keys select a linked organization with X-MindBill-Org-Id; missing selection returns 400 org_required. Singular /organization routes use that selected organization. Fixed organization keys and browser sessions cannot switch organizations.
Configure APP_ORIGIN as the frontend’s exact origin: scheme, host, and port, with no path or trailing slash. Live sessions require HTTPS; sandbox also accepts loopback HTTP. For separate frontend and backend hosts, apply explicit allowed-origin CORS and your existing CSRF protections, then use a component getSession callback with your authenticated fetch.
Assign browser permissions from your roles
Grant only the operations the signed-in user needs. Saved settings require a separate admin-authorized organization:manage session; ordinary bill creators can read masked profile choices with bills:create.
bills:createAtomically create and submit an immutable bill snapshot.bills:readRead bill review data, status, balances, and available actions.bills:actPost payment, close, correct, or start payer-review actions allowed by the bill state.documents:readList and open bill and RFA documents.payers:readSearch claims administrators, diagnosis and postal codes, and preview delivery routes.organization:manageRead and update organization settings, providers, locations, W-9, and custom claims administrators. Requires an organization-wide session.autofill:runExtract report suggestions for human review. Requires explicit operator delegation from autofill:write, written agreement, and the organization’s reportAutofill capability; no customer or bill scope. The developer console cannot mint this permission.team:manageList and update existing MindBill login accounts. Requires explicit delegation from a server key with orgs:team:write; organization:manage does not include it. Does not create users or change your host application roles.rfas:readRead authorization requests, status summaries, and delivery evidence.rfas:createCreate unsigned authorization drafts.rfas:editEdit authorized RFA details and supporting documents.rfas:actPerform permitted RFA workflow actions, including explicit transmission.rfas:signSign an RFA with the required signer authorization.eors:readRead normalized EOR data and original payer documents when available.RFA permissions require an organization-wide session and treatment access. Grant only the operations your user may perform; document preview also needs documents:read. See the RFA guide.
Example role mapping
const permissionsByRole = {
billing_admin: [
"bills:create", "bills:read", "bills:act",
"documents:read", "payers:read", "eors:read",
],
biller: [
"bills:create", "bills:read",
"documents:read", "payers:read", "eors:read",
],
viewer: ["bills:read", "documents:read", "eors:read"],
} as const;This mapping does not grant settings access. Add organization:manage only after checking the user’s practice-administration role.
Add a server route for your stack
Choose your stack below. Each browser-session recipe intentionally refuses to mint a token until you implement the host authorization adapter. It must authenticate the user, enforce your feature flag and role, and return the correct customer credential.
Required host authorization adapter
Required host adapter (YOUR code, not a MindBill export):
authorizeBillingSession(request) must authenticate the signed-in user, enforce the
billing feature flag and role, and resolve their active customer using SERVER-owned
session/membership data. Return { subject, apiKey, permissions, resource? }.
Deny unauthenticated/unauthorized requests with your framework's 401/403 response.
Select apiKey from your secret-manager mapping for that customer's MindBill organization.
Never accept API keys, tenant/org IDs, subjects, permissions or allowedOrigin from clients.
subject is audit identity, NOT tenant isolation. Organization-scoped sessions can access
all bills in that organization; never share them across unrelated customers.
Full workspace: bills:create, bills:read, bills:act, documents:read, payers:read, eors:read.
Read-only roles: bills:read, documents:read, payers:read, eors:read.
Case-only endpoint: authorize the host case and resolve its saved bill ID server-side,
set resource: { billId }, and omit bills:create.
Separate admin-only settings endpoint: organization:manage. Never grant this to all billers.
APP_ORIGIN is your FRONTEND origin (scheme + host + port, no path/trailing slash), not
the API server URL. Sandbox: http://localhost:3000; live: https://app.example.com.
For multiple frontend origins, validate against a server allowlist; never reflect Origin.
For split frontend/backend hosts, add exact-origin credentialed CORS, existing CSRF
protection, and a getSession callback with credentials: "include" where required.
Keep the host adapter fail-closed until connected to real authentication.Uses the stack selected above. Open your project first; review the instructions before running your agent.
Conductor creates a workspace in its first available repository. VS Code asks where to save a reusable prompt. These links require the desktop app; on mobile, copy or download the brief for later.
Codex, Flowcode, Claude Code, or another editor
Use Copy integration brief or Download brief above, open your project in your editor, and paste the brief into its coding agent. Use this portable option when a supported direct launcher is not available.
Supported launch formats: Cursor, VS Code, Conductor. Links contain only public setup instructions—never keys, patient information, or your repository path.
This route is intentionally fail-closed. Connect the required host authorization adapter below; copying code alone is not a completed integration.
// app/api/mindbill/session/route.js (App Router)
export async function POST(request) {
if (!process.env.APP_ORIGIN || request.headers.get("origin") !== process.env.APP_ORIGIN)
return Response.json({ error: "Origin not allowed" }, { status: 403 });
try {
const access = await authorizeBillingSession(request);
return Response.json(await mintSession(access), { headers: { "Cache-Control": "no-store" } });
} catch {
// Map your adapter's auth failures to 401/403; never forward upstream bodies.
return Response.json({ error: "Billing session unavailable" }, { status: 503 });
}
}
// Server only. Replace this placeholder with your EXISTING auth/tenant/role checks.
async function authorizeBillingSession(_request) {
throw new Error("Connect existing authentication and customer-key mapping first");
}
async function mintSession(access) {
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: process.env.APP_ORIGIN,
permissions: access.permissions,
...(access.resource ? { resource: access.resource } : {}),
expiresIn: 900,
}),
signal: AbortSignal.timeout(10000),
cache: "no-store",
});
if (!response.ok) throw new Error("Unable to start billing session");
return response.json();
}Full brief for Cursor, Codex, or Claude Code
# MindBill implementation brief
Frontend: React; backend: Next.js
Docs: https://docs.mindbill.org/learn/quickstart
Sandbox key: https://platform.mindbill.org/settings/api-keys
Install: pnpm add @mindbill/react@latest
Required host adapter (YOUR code, not a MindBill export):
authorizeBillingSession(request) must authenticate the signed-in user, enforce the
billing feature flag and role, and resolve their active customer using SERVER-owned
session/membership data. Return { subject, apiKey, permissions, resource? }.
Deny unauthenticated/unauthorized requests with your framework's 401/403 response.
Select apiKey from your secret-manager mapping for that customer's MindBill organization.
Never accept API keys, tenant/org IDs, subjects, permissions or allowedOrigin from clients.
subject is audit identity, NOT tenant isolation. Organization-scoped sessions can access
all bills in that organization; never share them across unrelated customers.
Full workspace: bills:create, bills:read, bills:act, documents:read, payers:read, eors:read.
Read-only roles: bills:read, documents:read, payers:read, eors:read.
Case-only endpoint: authorize the host case and resolve its saved bill ID server-side,
set resource: { billId }, and omit bills:create.
Separate admin-only settings endpoint: organization:manage. Never grant this to all billers.
APP_ORIGIN is your FRONTEND origin (scheme + host + port, no path/trailing slash), not
the API server URL. Sandbox: http://localhost:3000; live: https://app.example.com.
For multiple frontend origins, validate against a server allowlist; never reflect Origin.
For split frontend/backend hosts, add exact-origin credentialed CORS, existing CSRF
protection, and a getSession callback with credentials: "include" where required.
Keep the host adapter fail-closed until connected to real authentication.
"use client";
import {
ConnectedBillingWorkspace, ConnectedBillLifecycle,
BillSubmissionForm, BillingSettings, type BillSubmissionFormProps,
} from "@mindbill/react";
// Sidebar -> /billing. The workspace owns bill selection and detail navigation.
export function BillingPage({ canManageBilling }: { canManageBilling: boolean }) {
return <ConnectedBillingWorkspace
sessionEndpoint="/api/mindbill/session"
showSettings={canManageBilling}
billingSettings={{ sessionEndpoint: "/api/mindbill/settings-session" }}
style={{ height: "calc(100dvh - 96px)", minHeight: 0 }}
onCreateBill={() => { window.location.href = "/billing/new"; }}
/>;
}
// Case -> Billing tab after Report. Host server authorizes the case and resolves
// its saved bill ID; return a bill-scoped session from this separate host route.
export function CaseBilling({ caseId, billId }: { caseId: string; billId: string }) {
return <ConnectedBillLifecycle billId={billId}
sessionEndpoint={"/api/cases/" + encodeURIComponent(caseId) + "/billing-session"} />;
}
// Map existing structured case data to BillSubmissionInput, persist result.billId
// using your existing case metadata/API, and keep unknown fields editable.
export function NewBill({ canManageBilling, ...billProps }: Pick<
BillSubmissionFormProps, "initialBill" | "attachments" | "onSubmitted"
> & { canManageBilling: boolean }) {
return <BillSubmissionForm {...billProps}
{...(canManageBilling ? { billingSettings: {
sessionEndpoint: "/api/mindbill/settings-session"
} } : {})}
sessionEndpoint="/api/mindbill/session" />;
}
// Optional standalone settings page; the workspace already has a Settings tab.
// This administrator-only endpoint grants organization:manage.
export function PracticeBillingSettings() {
return <BillingSettings sessionEndpoint="/api/mindbill/settings-session" />;
}
// app/api/mindbill/session/route.js (App Router)
export async function POST(request) {
if (!process.env.APP_ORIGIN || request.headers.get("origin") !== process.env.APP_ORIGIN)
return Response.json({ error: "Origin not allowed" }, { status: 403 });
try {
const access = await authorizeBillingSession(request);
return Response.json(await mintSession(access), { headers: { "Cache-Control": "no-store" } });
} catch {
// Map your adapter's auth failures to 401/403; never forward upstream bodies.
return Response.json({ error: "Billing session unavailable" }, { status: 503 });
}
}
// Server only. Replace this placeholder with your EXISTING auth/tenant/role checks.
async function authorizeBillingSession(_request) {
throw new Error("Connect existing authentication and customer-key mapping first");
}
async function mintSession(access) {
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: process.env.APP_ORIGIN,
permissions: access.permissions,
...(access.resource ? { resource: access.resource } : {}),
expiresIn: 900,
}),
signal: AbortSignal.timeout(10000),
cache: "no-store",
});
if (!response.ok) throw new Error("Unable to start billing session");
return response.json();
}
// Host adapter example: rename these source fields to your existing structured data.
// This med-legal adapter omits treatment charges and line-level diagnosis pointers.
// For professional treatment, use /learn/treatment-quickstart.
// Do not infer clinical codes or upload every case document during SDK setup.
export function billFromCase(caseRecord) {
return {
externalId: String(caseRecord.id),
patient: {
firstName: caseRecord.patient.firstName ?? "",
lastName: caseRecord.patient.lastName ?? "",
dateOfBirth: caseRecord.patient.dateOfBirth ?? "", // YYYY-MM-DD
address: {
line1: caseRecord.patient.address?.line1 ?? "",
city: caseRecord.patient.address?.city ?? "",
state: caseRecord.patient.address?.state ?? "",
postalCode: caseRecord.patient.address?.postalCode ?? "",
},
},
claim: {
claimNumber: caseRecord.claimNumber ?? "",
employer: caseRecord.employerName,
dateOfInjury: caseRecord.dateOfInjury,
},
service: { date: caseRecord.dateOfService ?? "" },
diagnoses: caseRecord.diagnosisCodes ?? [],
serviceLines: (caseRecord.billingLines ?? []).map((line) => ({
code: line.code, modifiers: line.modifiers, units: line.units,
})),
// Optional: billingProvider, renderingProvider, serviceLocation from
// your saved host settings. Otherwise let the user supply/review them.
};
}
// Only pass the finalized report. An empty list leaves upload to the user.
// documentId identifies a HOST document, not a MindBill document.
export function finalReportSources(documentId) {
if (!documentId) return [];
return [{
id: documentId, fileName: "final-report.pdf",
documentType: "final_report", autoAttached: true, removable: true,
loadBlob: async () => {
// Existing authenticated host endpoint: enforce document/case ownership.
const response = await fetch("/api/documents/" + encodeURIComponent(documentId), {
credentials: "same-origin", cache: "no-store",
});
if (!response.ok) throw new Error("Unable to load finalized report");
return response.blob();
},
}];
}
// Requires @mindbill/react >=0.62.0.
"use client";
import { BillSubmissionForm, type BillSubmissionFormProps,
type BillSubmissionInput } from "@mindbill/react";
export function ProfileBasedBill({ initialBill, canManageBilling }: {
initialBill: BillSubmissionInput; canManageBilling: boolean;
}) {
return <BillSubmissionForm initialBill={initialBill}
sessionEndpoint="/api/mindbill/session"
profileDisplay="compact"
{...(canManageBilling ? { billingSettings: {
sessionEndpoint: "/api/mindbill/settings-session"
} } : {})} />;
}
// The connected form reads saved choices automatically when profileOptions is omitted.
// The billing endpoint needs organization-wide bills:create, not organization:manage.
// billingSettings enables the prebuilt Add/edit settings flow. Its SEPARATE endpoint
// must enforce the administrator role server-side and grant organization:manage.
// Hiding a button is not an authorization check. Omit billingSettings for other users.
// Existing case values remain unchanged until the user explicitly selects a profile.
// Host-managed alternative: explicit choices override automatic organization lookup.
export function HostProfileBill({ initialBill, savedBillingProvider,
savedRenderingProvider, savedServiceLocation }: {
initialBill: BillSubmissionFormProps["initialBill"];
savedBillingProvider: NonNullable<BillSubmissionInput["billingProvider"]>;
savedRenderingProvider: NonNullable<BillSubmissionInput["renderingProvider"]>;
savedServiceLocation: NonNullable<BillSubmissionInput["serviceLocation"]>;
}) {
const profileOptions = {
billingProviders: [{ id: "practice-1", label: "Main practice", value: savedBillingProvider }],
renderingProviders: [{ id: "doctor-1", label: "Rendering doctor", value: savedRenderingProvider }],
serviceLocations: [{ id: "office-1", label: "Main office", value: savedServiceLocation }],
};
return <BillSubmissionForm initialBill={initialBill} profileOptions={profileOptions}
profileDisplay="compact" sessionEndpoint="/api/mindbill/session" />;
}
// profileOptions={} also suppresses the organization-wide lookup when it is unwanted.
// Bill-scoped sessions cannot read organization-wide choices. Do not widen access.
// Omit profileDisplay (or use "expanded") to show all fields.
// Selecting a profile does not mutate the saved profile.
// SSN-backed choices retain savedProviderId; the server resolves the identifier.
// Never put taxIdLast4 in taxId, browser storage, telemetry or agent prompts.
// Settings supports taxIdType: "EIN" | "SSN" (default EIN).
// Saved SSNs are encrypted. Responses contain empty taxId, taxIdLast4 and
// taxIdConfigured. In settings: blank preserves, replacement changes, clear removes.
// Custom settings API writes: omit taxId to preserve; taxId: "" explicitly clears.
// Do not send response-only taxIdLast4/taxIdConfigured in a settings write.
Notification ownership (choose deliberately):
Notifications default OFF for every integration.
Admin recipient list (React >=0.52.0, including digests): add NotificationRecipientsSettings (alias
ConnectedNotificationRecipientsSettings). Any authorized email can be invited; no
console or host account is needed. Use memoized { load, invite, disable } with an
identityKey that changes for administrator/practice/environment. The adapter calls
your authenticated admin server, not the Partner API from a browser. Protect CSRF.
GET /partner/v2/notifications/recipients?offset=0 lists recipient states.
POST /partner/v2/notifications/recipients/{externalUserId}/invitations accepts only
requestId, email, audience, statusUpdates, agingDays, quietHours, reportDigest. Validate/authorize
these fields and preserve the same UUID requestId on unchanged/uncertain retries.
Resolve a stable opaque recipient ID and organization/environment from server state.
Return upstream data, not its envelope. Strip private sandbox previewUrl unless you
are implementing a purpose-built protected preview. Never log capability URLs.
Defaults: assigned_bills, no categories, reportDigest off, quiet hours 7 pm-7 am Pacific.
The email owner reviews scope/categories and confirms a 48-hour invitation; opening
the link alone does nothing. Pending invitations remain OFF. An administrator cannot
consent for somebody else. Unsubscribe/disable invalidates old invitation links.
Invitation deliveryStatus is not enrollment: even sent is only transport acknowledgment.
Do not automatically resend unknown outcomes or loop on rate limits. Changed input
with the same UUID is a conflict. Sandbox sends no invitations or alerts.
Supports status/payment and 30/60/90-day aging alerts, plus opt-in billing-activity
digests: reportDigest is off | daily | weekly. Scheduled at 9 am Pacific (Monday
for weekly), skipping empty periods. Counts only: no patient details, attachments,
financial balances or confirmed cash totals. Changing cadence needs fresh consent.
Forward reportDigest through your host adapter and include it in preference snapshots.
Practice scope includes future bills; assigned_bills
requires trusted server assignment sync (below). A new invitation disables the old
subscription until reconfirmed; email/scope changes require assignment resync.
Personal preferences: add NotificationSettings (ConnectedNotificationSettings alias) to your existing
settings page. Use a memoized host-server adapter { load, save, unsubscribe } and an
identityKey that changes with user/practice/environment. No admin key in the browser.
The widget submits preference fields + explicit consent only; identity/email/audience,
verification timestamps and bill IDs are server-owned. Protect mutations with CSRF.
GET -> PUT/DELETE -> reload GET; mutation acknowledgments are not settings snapshots.
Any changed enabled settings require fresh consent (including quiet hours). Persist
the real consent receipt and reuse it for identical retries; do not renew consent in jobs.
Copyable React and trusted server templates: https://docs.mindbill.org/guides/notifications
1. MindBill can send alerts to users without console accounts, including doctors
authorized for only specific bills. Your SERVER supplies the trusted assignments.
Choose audience: "assigned_bills" for those doctors, "practice" only for users
authorized to receive updates about every bill in the managed practice.
GET/PUT/DELETE /partner/v2/notifications/recipients/{externalUserId}
Server API key with orgs:write + X-MindBill-Org-Id for a partner-managed practice.
Check GET availability first. This feature requires MindBill rollout/activation;
do not claim success while disabled. Browser session tokens cannot enroll users.
Authenticate the host user, verify their email and the requested access scope,
and collect explicit opt-in before PUT. Use real server-recorded consent and
verification timestamps, never Date.now() as a substitute for proof of consent.
For assigned_bills, PUT/DELETE the server-only association at
/partner/v2/notifications/recipients/{externalUserId}/bills/{billId}.
Resolve bill/user access from existing host permissions, not browser input or NPI.
Assignment is not consent; enrollment is still required. Remove associations when
access ends and reconcile them when your host bill/user assignment changes.
Associate existing bills after opt-in and new bills during trusted creation.
Empty assignments send nothing; earlier events/milestones are not replayed.
Revoke via recipient DELETE when membership ends. Email/contact suggestions are
NOT notification consent. Never automatically enroll courtesy-copy recipients.
https://docs.mindbill.org/guides/notifications
2. For custom content or delivery, signed webhooks remain optional: verify raw-body
signatures, deduplicate event IDs, handle retries/out-of-order delivery and reconcile
current bill state. Never broadcast all practice events to a case-only user.
3. Keep notification messages PHI-free and route users through authenticated screens.
Test sandbox consent, unsubscribe, stale retries, tenant isolation and disabled gates.
Rendering billing components does not automatically turn notification delivery on.
Inspect the existing app first. Do not replace authentication or run a migration.
1. Gate sidebar Billing, case Billing tab and server endpoints with the EXISTING tenant/user
feature flag. Verify enrolled users see both entries and others see neither.
2. Render ConnectedBillingWorkspace in a bounded, shrinkable layout. Components include their own styles.
Case Billing: creation form until submitted, then lifecycle. Settings: admin-only.
3. Implement the selected server recipe AND host auth adapter using server-owned tenant
mapping. Browser sees short-lived tokens only. Never hardcode a subject or API key.
4. Reuse existing case metadata for externalId and returned billId. externalId is correlation,
not an idempotency/uniqueness guarantee. Never recreate a bill on rerender.
Avoid local copies of bills/payments/EORs. If no durable mapping exists, explain the
smallest necessary persistence change first; do not run a speculative migration.
5. Prefill known patient/address/injury/claim/service/diagnosis/code/modifier values from
structured host data. Leave unknown fields editable; never fabricate clinical values.
Do not add regex/AI report parsing during SDK setup. Use existing extraction and user
review. Confirm the canonical payer directory choice before submission.
6. Autoattach ONLY the finalized report, not every case document, medical record or audio
file. Let users upload/select proof of service and additional supporting PDFs.
7. Decide data ownership: host-managed providers/locations/W-9 OR MindBill BillingSettings.
Avoid duplicated storage and silently copying sensitive tax identifiers.
8. Keep API keys/tokens/SSN/tax IDs/PHI out of logs, localStorage and coding-agent prompts.
9. Verify sandbox Sent -> Accepted/Rejected -> Processed, second review/routing, payments,
notes, attachments, All Bills visibility and laptop/mobile scrolling. Sent bills waiting
for payers are not follow-up tasks but MUST remain discoverable.
10. Choose MindBill-hosted notifications with verified consent and server-owned bill
assignments, or custom webhook delivery. For webhooks, verify raw-body signatures,
deduplicate event IDs and reconcile current state. Keep emails PHI-free.
https://docs.mindbill.org/guides/notifications
11. Test token refresh, 401/403, cross-tenant IDs, disabled feature flags, non-admin settings,
missing origin config, empty/error states. Keep sandbox and live keys/data separate.
12. Run typecheck/build/tests; report changed files, actual package versions and unfinished
host adapters. An unwired auth placeholder is NOT a finished integration.Return session responses with Cache-Control: no-store. Map authentication failures to 401 or 403; return a generic error for upstream failures. Never expose keys or upstream error bodies.
Restrict a session to one bill
For a case billing tab, authorize access to the host case and resolve its saved MindBill bill ID on the server. Set resource: { billId } and omit bills:create. Use an organization-wide session for the full workspace or saved-profile lookup.
See the browser-session reference for token fields, expiration, and resource rules. For API-only integrations, use the server key directly; no session route is needed.