BillingOS Documentation Audit
The docs cover a small, well-scoped surface (34 pages) but the React SDK and Node SDK describe the same domain objects with different field names, different argument shapes, and different type schemas — and the marketing claim that "you don't need to learn Stripe" is undermined by the go-live page itself. Several method tables advertise capabilities (invoices, payment methods, portal sessions) that have no documentation page at all.
1. Subscription object is documented two different ways (critical)
Location: /sdk/hooks/use-subscription vs /server-sdk/subscriptions
Problem: The React SDK's Subscription interface uses snake_case keys (customer_id, price_id, current_period_start, cancel_at_period_end, trial_start, trial_end, created_at, updated_at) and lists 7 statuses including incomplete_expired and unpaid. The Node SDK's Subscription interface for the same object uses camelCase (customerId, currentPeriodStart, cancelAtPeriodEnd) and lists only 5 statuses. The mutation methods diverge identically: useUpdateSubscription accepts price_id/cancel_at_period_end, while server updateSubscription accepts priceId/cancelAtPeriodEnd. /guides/manage-subscriptions confirms the React shape by reading subscription.price_id and subscription.current_period_end directly.
Consequence: Any developer wiring server logic from a client subscription object (or vice versa) ships broken code. Agents that copy a server example and feed its result into a React hook silently match on undefined fields. The status mismatch means client code can encounter statuses (incomplete_expired, unpaid) the server-side type never declares — TypeScript users get exhaustiveness errors with no documented source of truth.
The fix: Pick one casing for the wire format, document it once, and link both pages to it. If the API truly returns snake_case JSON and the Node SDK transforms it to camelCase, say so explicitly with a "wire vs SDK" table. Reconcile the status enum on both pages.
2. "You don't need to learn Stripe" — but the go-live page disagrees (significant)
Location: /index.md vs /guides/go-live-checklist.md vs /sdk/components/checkout-modal.md
Problem: The landing page says "You don't need to learn Stripe. BillingOS handles all the payment complexity behind the scenes." The SDK overview reinforces with "No Stripe SDK … No PCI compliance … No webhook handling." But the go-live checklist explicitly references Stripe key rotation and live keys as developer responsibilities, and the CheckoutModal docs state "Card details are handled entirely by Stripe — your app never processes or stores payment information."
Consequence: Teams choose BillingOS expecting to skip Stripe entirely, then discover at go-live time that they own Stripe key management and certain Stripe-side concerns (disputes, payouts, refund operations) that the docs never delineate. The framing also leaves developers unsure which billing concepts are theirs to own and which BillingOS abstracts.
The fix: Replace the absolute claim with a scoped one: "BillingOS handles Stripe SDK integration, PCI compliance, and webhook plumbing — you'll still need a Stripe account and a small set of operational responsibilities." Add a "What Stripe still owns" panel.
3. Method tables advertise endpoints that have no docs page (critical)
Location: /sdk/client.md
Problem: The BillingOSClient reference lists full method groups for Invoices (getInvoice, listInvoices), Payment methods (listPaymentMethods, removePaymentMethod, setDefaultPaymentMethod), and Portal (portal.createSession, portal.getSessionStatus). The sitemap confirms only 34 URLs exist — none of them are dedicated invoice, payment-method, or portal-session reference pages. There is no FAQ, errors, changelog, webhooks, or rate-limits page either.
Consequence: Developers see a method name with no parameter schema, no return type, no examples. Agents indexing the docs cannot resolve what listInvoices(params?) accepts — params is literally undefined. Anyone shipping a custom invoice list or "Manage payment methods" UI has to read the SDK source to make progress.
The fix: Either add real reference pages for invoices, payment methods, and portal sessions, or remove those methods from the public reference until they're documented. At minimum, link each row in the method table to a section that defines input / return shape.
4. checkEntitlement is called two incompatible ways (critical)
Location: /sdk/client.md vs /server-sdk/entitlements.md
Problem: The BillingOSClient reference says checkEntitlement(input) — a single object argument. The Node SDK page documents the same method as positional (customerId, featureKey). They cannot both be the published signature.
Consequence: Half the developers reading the docs will write the wrong call shape on the first try. Agents copying the BillingOSClient form into a Node SDK file will get a TypeScript error or, worse, a silent runtime failure if positional args are accepted loosely.
The fix: Pick one calling convention, fix the mismatched page, and add the parameter table directly to both pages instead of relying on the reader to cross-check.
5. Two pages document idempotency for the same usage event differently (significant)
Location: /sdk/hooks/use-track-usage.md vs /server-sdk/usage.md
Problem: The server SDK's trackUsage documents a top-level idempotencyKey parameter. The React useTrackUsage page never documents a top-level idempotency parameter at all — it surfaces metadata (typed Record<string, string>) as the open-ended key/value bag, with no canonical "idempotency" guidance. Neither page acknowledges the other.
Consequence: Developers using both sides (a common pattern: optimistic client tracking with a server backstop) cannot tell whether a single dedup key flows end-to-end. Client-side double-submits will be deduped on the server but not guarded on the client; clients that try to pass an idempotency key will guess at where to put it.
The fix: Promote idempotencyKey to a top-level parameter on both APIs and document it with the same name. Add an explicit "idempotency" section that names the field and shows it producing the same dedup result on client and server.
6. expiresAt is a Date and an ISO string at the same time (significant)
Location: /server-sdk/session-tokens.md vs /guides/session-tokens.md
Problem: The server-SDK reference declares expiresAt: Date. The session-tokens guide shows the JSON response with "expiresAt": "2025-01-15T12:00:00Z" — a string. There is no note explaining (de)serialization.
Consequence: TypeScript developers typing the server response against the documented Date type will get a string at runtime; calling .getTime() blows up. Anyone serializing the SDK return value to send to a frontend is left guessing whether they need .toISOString().
The fix: State explicitly that the wire format is an ISO 8601 string, and that the Node SDK parses it into a Date (or doesn't — pick one). Show both shapes side-by-side.
7. revokeSessionToken argument is ambiguous — security risk (critical)
Location: /server-sdk/session-tokens.md
Problem: The example calls billing.revokeSessionToken("token_id_here") with no clarification of what "token id" means. The token strings the docs show elsewhere look like bos_session_test_*. There is no separate "token ID" surfaced in createSessionToken's response schema.
Consequence: Developers either pass the raw token (which forces them to persist the secret in order to revoke it later — a significant security regression) or pass an ID that doesn't exist. This is the exact path where an unclear argument turns into a real auth incident: secrets stored where they shouldn't be, or revocation calls that silently no-op.
The fix: Document the exact field returned from createSessionToken that maps to this argument. If revocation requires the secret token string, say so and explain the operational implications. If a separate id is returned, surface it in the response schema.
8. customerId means two different things (significant)
Location: /server-sdk/usage.md vs /server-sdk/customers.md vs /guides/gate-features.md
Problem: server-sdk/usage.md describes the customerId parameter as "Customer external user ID." But server-sdk/customers.md shows getCustomer("cus_123") — a cus_* BillingOS-internal ID. The Gate Features guide reinforces the confusion: it imports BillingOS from @billingos/node and calls billing.trackUsage({ customerId: userId, ... }), treating the app's own user ID as customerId directly.
Consequence: Half the integrations will track usage against the wrong customer object. Errors won't surface immediately — they'll surface as "why is this user's usage 0?" weeks later in production.
The fix: Pick one meaning for customerId everywhere (most APIs use the internal ID), and introduce a separate externalUserId parameter where lookup-by-external-id is needed. Update every example and parameter description to match.
9. Hook customer-scoping rules are inconsistent (significant)
Location: /sdk/hooks/use-feature.md vs /sdk/hooks/use-entitlements.md vs /guides/track-usage.md
Problem: useFeature("export_pdf") takes only a feature key and infers the customer from session context. useCheckEntitlement requires an explicit customerId. useIsApproachingLimit("cus_123", "api_calls", 80) also requires an explicit customer ID. None of the pages explain why — when does the SDK infer the customer, when do you have to pass one?
Consequence: Developers guess. They pass customerId everywhere "to be safe" and end up with checks that don't update when the active session changes, or they omit it and get unscoped data leaks across users in B2B scenarios.
The fix: Add a "Customer scoping" section to the React SDK overview explaining the rule (e.g., "Hooks that take feature keys infer from session; hooks that admin/aggregate across customers require an explicit ID"). Then make every hook page reference it.
10. Two parallel checkout APIs, no guidance on which to use (significant)
Location: /sdk/client.md
Problem: The BillingOSClient lists both top-level createCheckout(input) / confirmCheckout(clientSecret, paymentMethodId) and client.checkout.createSession(input) / getSession(sessionId) / confirmPayment(secret, pmId). The "Checkout modal (iframe)" header only suggests the second is for the modal, but the modal has its own component (<CheckoutModal />) that you don't drive directly.
Consequence: A developer building a custom checkout sees two near-identical APIs with no decision tree and picks one at random. The one they pick may be the internal-only iframe surface, leading to confusing iframe-postMessage errors with no documented cause.
The fix: Add a one-sentence "When to use" above each subgroup. If client.checkout.* is internal to the iframe component, mark it @internal and remove from the public reference.
11. Money units never declared (significant)
Location: /sdk/hooks/use-products.md vs /sdk/components/pricing-table.md vs /sdk/components/upgrade-nudge.md
Problem: useProducts example hard-codes "amount in cents" (2900 = $29.00). PricingTable, UpgradeNudge, and CheckoutModal docs never mention minor units. UpgradeNudge's suggestedPlan.price is described only as "Amount, currency, interval" with no schema.
Consequence: Developers building custom UIs from useProducts divide by 100 in some places and not others. Agents extracting price values for display will render $2900 instead of $29.00.
The fix: Add a single global "Money values are integers in the smallest currency unit (e.g., cents for USD)" note in the SDK overview and link every price-bearing field's description to it. Provide the actual price schema for suggestedPlan.
12. No webhook docs, but entitlements "update automatically" (significant)
Location: /sdk/overview.md and /guides/accept-a-payment.md
Problem: The SDK overview promises "No webhook handling — Entitlements update automatically after checkout." The accept-a-payment guide says "The customer's entitlements are immediately updated" after onSuccess fires. No page explains the propagation model: is it polling, server-sent events, a refetch on useEntitlements, or a server-side cache invalidation tied to Stripe webhooks BillingOS handles for you?
Consequence: When an entitlement doesn't update immediately (network blip, race with onSuccess, server-side webhook lag), developers have no documented retry/refetch strategy and no way to debug whether the issue is BillingOS, their session token, or their query cache.
The fix: Document the propagation mechanism in one paragraph (e.g., "BillingOS receives the Stripe webhook and invalidates the cached entitlements for that customer; the React SDK refetches via TanStack Query on the next render"). Document the expected p99 latency and a manual refetch() escape hatch.
13. checkUsage() is the engine behind UpgradeNudge but its signature is undocumented (significant)
Location: /guides/upgrade-nudges.md vs /sdk/client.md
Problem: The upgrade-nudges guide says "The SDK calls checkUsage() to see if the user is near a limit" and shows an example that calls client!.checkUsage() with no arguments. BillingOSClient's method tables include getUsageMetrics and trackUsage but no entry for checkUsage at all. There is no parameter schema, no return type, no documentation of how to scope it to a feature or a customer.
Consequence: The component the docs market most heavily for "automated upgrade prompts" runs on a method developers cannot call directly with confidence. Anyone building a custom nudge surface has to reverse-engineer the method from the SDK source.
The fix: Add a checkUsage entry to the BillingOSClient reference with arguments, return shape, and an example. State explicitly whether it scopes to the session-token customer or accepts an explicit ID.
14. Snippet placeholders leak into the markdown surface that agents consume (significant)
Location: /sdk/provider.md and /guides/session-tokens.md
Problem: Both pages emit literal <Snippet file="session-token-endpoint.mdx" /> and <Snippet file="provider-setup.mdx" /> directives in the raw markdown surface that llms.txt and the markdown export use. The session-token endpoint is the single most-copied snippet in the docs — and the page that LLM consumers see is a placeholder, not the code.
Consequence: Agents and LLMs that consume the markdown directly (the entire point of llms.txt) see a placeholder instead of the actual session-token endpoint code. The auth path — the documentation surface BillingOS most needs agents to understand correctly — is the one most likely to be parsed wrong.
The fix: Inline the snippet content into the markdown export pipeline, or render snippets server-side before serving *.md URLs.
15. server-sdk/subscriptions.md omits methods and parameters the client reference promises (significant)
Location: /server-sdk/subscriptions.md vs /sdk/client.md
Problem: BillingOSClient documents cancelSubscription(id, immediately?) with an immediately flag, and listSubscriptions(params?). The Node SDK subscriptions page never documents the immediately parameter on cancelSubscription and never lists listSubscriptions at all.
Consequence: Developers writing server-side cancellation flows can't tell from the canonical Node-SDK reference whether immediately is supported, what its default is, or how proration works. Server-side subscription listing — a basic admin operation — has no documented entry point.
The fix: Bring the Node SDK subscriptions reference to parity with BillingOSClient: document every parameter on cancelSubscription, add listSubscriptions with its filter and pagination schema.
16. getCustomerByExternalId exists in the Node SDK but not in the client reference (minor)
Location: /server-sdk/customers.md vs /sdk/client.md
Problem: The server SDK Customers page documents getCustomerByExternalId. The BillingOSClient method tables list getCustomer(id), listCustomers(params?), updateCustomer, deleteCustomer — but no getCustomerByExternalId. The asymmetry is unexplained.
Consequence: Developers can't tell whether external-ID lookup is server-only by design (because external IDs shouldn't be exposed to browsers) or simply omitted from the client docs by oversight. Agents matching across pages will assume the omission is a bug.
The fix: Either add the method to BillingOSClient or add a one-line note on the Customers page: "Server-only — do not expose external user IDs to browsers." State the intent explicitly.
17. metadata typing varies across the SDK (minor)
Location: /sdk/components/checkout-modal.md vs /server-sdk/customers.md vs /sdk/hooks/use-entitlements.md vs /server-sdk/entitlements.md
Problem: metadata is typed as Record<string, string> on CheckoutModal, Record<string, any> on server-side Customer, Record<string, string> on the React Entitlement, and as a structured object { remaining?, resets_at?, type? } | null on the server-side EntitlementResponse. Same field name, four different shapes.
Consequence: TypeScript users round-tripping metadata across boundaries get type errors. Consumers of the EntitlementResponse metadata field can't tell if they're getting their custom strings back or BillingOS-controlled fields.
The fix: Reserve metadata for user-controlled Record<string, string> everywhere. Rename the EntitlementResponse field that carries remaining/resets_at/type to details or limits to avoid collision.
18. feature vs featureKey naming inconsistency (minor)
Location: /sdk/components/feature-gate.md and /sdk/components/upgrade-prompt.md vs /sdk/hooks/use-feature.md and /sdk/components/usage-display.md
Problem: <FeatureGate> and <UpgradePrompt> take a prop named feature. useFeature and <UsageDisplay> use featureKey. Same string value, different prop names across the same SDK.
Consequence: Agents pattern-matching across files write featureKey on a <FeatureGate> and lose the gate silently (TypeScript may catch it, but runtime defaults are forgiving).
The fix: Rename to featureKey everywhere (deprecate feature with a console warning) and document the alias during the transition.
19. CompactUsageDisplay requires a featureKey that's optional on the parent (minor)
Location: /sdk/components/usage-display.md
Problem: UsageDisplay's featureKey is optional ("Show usage for a specific feature. Omit to show all metered features."). CompactUsageDisplay's featureKey is required. Same prop name, different semantics. The page never explains why.
Consequence: Developers swapping between the two components get a runtime/type error with no signposted reason.
The fix: Either make the compact variant accept an optional key (and pick a sensible "show first metered feature" default), or rename the required prop to make the constraint visible.
20. Quickstart depends on an undefined auth helper (minor)
Location: /quickstart.md
Problem: The quickstart's session-token endpoint flow assumes the developer has a way to identify the currently-logged-in user, but the page never names a concrete auth provider, never shows a helper definition, and never points to a "implement this in your auth layer" note. The Mintlify snippet is referenced but the public markdown leaves the gap visible.
Consequence: Developers running through the quickstart hit a missing function reference and can't tell whether the SDK provides the helper or they're meant to write it.
The fix: Replace the placeholder with a concrete example tied to a common auth provider (Clerk, NextAuth, Supabase Auth) or annotate the line with // from your auth provider — see [link].
21. Test-card guidance differs across pages (minor)
Location: /quickstart.md vs /guides/accept-a-payment.md vs /guides/go-live-checklist.md
Problem: The accept-a-payment guide documents three test cards (success 4242…, 3DS 4000…3220, declined 4000…0002). The quickstart references only the success card. The go-live checklist tells developers to "test one real payment" without enumerating which test scenarios should have been covered first.
Consequence: Developers running quickstart-only never test failure paths (declined cards, 3DS challenges) and discover those paths in production. The go-live checklist provides no signal about which test cases must pass before flipping to live keys.
The fix: Lift the three-card table into a shared snippet referenced from quickstart, accept-a-payment, and go-live. Make the go-live checklist explicitly require successful test runs against each scenario.
22. Domain inconsistency between dashboard and docs (minor)
Location: /sdk/installation.md
Problem: The installation page links the BillingOS Dashboard as https://app.billingos.com. Every other URL in the docs surface is on billingos.dev.
Consequence: Either the .com link 404s (and developers can't find their dashboard) or it works (and the dev/com domain split is unexplained, raising trust questions about which is the "real" product surface).
The fix: Use one domain consistently and explain any cross-domain split (dev = docs, com = dashboard) in a single sentence on the index page.
23. BILLINGOS_API_URL is an orphaned configuration knob (minor)
Location: /server-sdk/overview.md
Problem: The Node SDK overview lists BILLINGOS_API_URL as an environment variable. The string never appears anywhere else in the docs surface — no guide explains when to set it, no "self-hosted" or "regional endpoint" page references it, no FAQ entry mentions it.
Consequence: Developers debugging connectivity issues can't tell whether BILLINGOS_API_URL is for staging environments, regional routing, on-prem deployments, or a relic. Agents asked to point a server SDK at a custom endpoint will set it, then have nowhere to verify the expected format.
The fix: Either document a concrete use case for BILLINGOS_API_URL (with examples of valid values) or remove it from the env var list until it's a supported public knob.
24. PricingTable has no documented loading, error, or empty-state behavior (minor)
Location: /sdk/components/pricing-table.md
Problem: <PricingTable /> is marketed as a drop-in pricing page. The full props table has no entries for loading state, error state, or what the component renders when planIds filters yield zero products. There is no onError callback documented either.
Consequence: Developers using <PricingTable /> as the entire pricing route can't customize what users see when the products API is slow or fails — the component is a black box for those states. Failures will surface as a user-visible blank section with no recovery path.
The fix: Document the default loading, error, and empty states. Expose loading / error / empty slot props or render-prop overrides, and document an onError callback to surface failures to the host app.
25. taxId accepts any string with no country-format guidance (minor)
Location: /sdk/hooks/use-checkout.md and /sdk/components/checkout-modal.md
Problem: Both surfaces accept a customer.taxId string with no validation rules, no per-country format reference, and no link to which tax-ID schemes BillingOS recognizes (VAT, GST, ABN, EIN, etc.).
Consequence: Developers either submit unvalidated tax IDs (which may fail server-side with no client-visible error) or build their own validators against an unknown spec. Tax compliance is the exact area where ambiguous input causes downstream invoicing problems.
The fix: Document the supported tax-ID schemes and their expected formats, or link to a reference that does. Surface a validation hook the SDK can run before submitting.
26. No npm or source-of-truth link for the published SDK packages (minor)
Location: Docs surface generally; sdk/installation.md
Problem: The docs reference @billingos/sdk and @billingos/node as installable packages but never link to their npm pages or a source repository. The GitHub repo behind the docs is the BillingOS marketing/dashboard app, not the SDK source.
Consequence: Developers evaluating BillingOS can't inspect the SDK source, file issues against it, or verify maintenance signals (last publish, version history, dependency tree). The "developer toolkit" framing is undermined by the missing trust signal.
The fix: Link npm pages for both packages from the installation page, and either open-source the SDKs or publish a public issue tracker.
27. llms.txt exists but no concatenated full-content export is linked (minor)
Location: /llms.txt
Problem: llms.txt lists the 34 pages of the docs surface but does not link to a concatenated full-content export. Combined with the snippet-leak issue (#14), agents that follow the llms.txt index and fetch each .md end up with placeholder directives in place of the most-copied auth code.
Consequence: Agent indexing is slower and incomplete. The documentation surface BillingOS most needs agents to understand correctly (auth) is the one most likely to be parsed wrong.
The fix: Generate and link a concatenated full-content export that inlines snippets, and keep llms.txt as the index.
What they do well
- Tight scope — 34 pages is small enough that a single audit pass can verify cross-page consistency, and the navigation is uncluttered.
- Pre-built component coverage is real — every advertised component (
PricingTable,CheckoutModal,CustomerPortal,FeatureGate, etc.) has a dedicated reference page with prop tables. - Both client and server SDKs are documented — many billing tools punt on the server side; BillingOS at least publishes a Node SDK reference.
Top 3 recommendations
- Pick one wire format and one casing convention for
Subscription,Customer,Entitlement, and reconcile the React vs Node SDK pages so the same field has the same name and type everywhere — including thecheckEntitlementsignature. - Stop saying "no Stripe" on the landing page. Replace with a specific list of what BillingOS abstracts (SDK, PCI, webhook plumbing) and what the developer still owns.
- Fill the reference holes the method tables already promise — invoices, payment methods, portal sessions,
checkUsage,listSubscriptions, errors, webhooks, rate limits. Right nowBillingOSClientlists 30+ methods and the docs explain maybe half of them.