BillPro Documentation Audit
The reference content is reasonably complete in prose, but the machine-readable layer is the weakest link: the OpenAPI spec never defines a production server, error responses are unreliable (not-found conditions return 422 "Bad Request"), the same field changes type between endpoints, two core integration code blocks are empty, and the primary API "Get started" page links to pages that 404. Layered on top is a consistent pattern of the Stripe integration being the less-documented path. An AI agent — or a careful human — following these docs literally will ship against the test environment with broken cross-references and unparseable errors.
1. The OpenAPI spec never defines a production server (critical)
Location: /reference/create-single-order and /reference/get-order (OpenAPI), vs /changelog/billpro-url-changes and /reference/integrate-with-custom-checkout-and-payment-iframe
Problem: Both OpenAPI specs whose servers block is visible list exactly one server: "url": "https://test.bill.pro/api/v1". The production base URL (https://app.bill.pro/) appears only in prose — the changelog and the iframe page's "How to go live" table (Test = https://test.bill.pro, Live = https://app.bill.pro) — and is never represented in any machine-readable spec. The interactive "Try it!" console therefore always targets the test environment.
Consequence: Any client generated from the spec (SDK codegen, Postman import, an AI agent reading the OpenAPI) will hard-code the test host and have no programmatic way to discover the live host. Developers test successfully, ship, and silently keep hitting test.bill.pro in production. The one place machines look for the base URL is wrong.
The fix: Add both servers to every spec, e.g. {"url": "https://app.bill.pro/api/v1", "description": "Production"} and {"url": "https://test.bill.pro/api/v1", "description": "Test"}, with production listed first.
2. The primary "Get started with the API" page links to pages that 404 (critical)
Location: /reference/billpro-api
Problem: The canonical onboarding page links Products → /docs/products and Orders → /docs/orders. Both return HTTP 404. The real pages are /reference/products-api and /reference/orders-api (confirmed in llms.txt, which contains no /docs/products or /docs/orders entries).
Consequence: The first page a new integrator reads sends them to dead ends for two of the three core resources. An agent crawling from the entry point loses the Products and Orders references entirely.
The fix: Repoint the links to /reference/products-api and /reference/orders-api. Add a link-check step to CI against the llms.txt inventory.
3. customer_id is a string in one response and an integer in another (critical)
Location: /reference/get-order vs /reference/create-customer
Problem: In the create-customer response, the alphanumeric identifier is returned as "customer_id": "EF6B58A4E9" (a string), alongside a separate numeric "id": 297. But in get-order responses, customer_id is an integer ("customer_id": 1, 371). Same field name, two incompatible types across endpoints.
Consequence: Strongly-typed clients and codegen'd SDKs deserialize one shape and throw on the other. An agent that stores the string customer_id from create-customer and later passes it to get-order (or compares it against get-order's integer customer_id) will mismatch records. This fails silently until runtime.
The fix: Pick one type for customer_id across all endpoints, and clarify which identifier the order endpoints expect — the create-customer response exposes both a numeric id and a string customer_id, so the docs must state explicitly which one feeds the order resources and keep its type consistent everywhere.
4. Not-found and bad-input conditions all return 422 with body {"error": "Bad Request"} (critical)
Location: /reference/get-order, /reference/create-single-order, /reference/create-customer
Problem: There is no 404 anywhere in the spec. "Order ID not found" (get-order) and "Missing or not found product ID" (create-order) are both documented as 422 responses carrying the literal body {"error": "Bad Request"} — a status code, a name, and a message that disagree three ways. Meanwhile create-customer's 422 uses an entirely different, field-level shape: {"email":["is invalid","can't be blank"], "first_name":["can't be blank"], ...}.
Consequence: Error handling is not a contract. A developer cannot branch on status code (not-found and validation both yield 422), cannot branch on body text ("Bad Request" means several different things), and cannot write one parser (generic {"error":...} vs field-level arrays). Agents that switch on error shape will mishandle failures across endpoints.
The fix: Return 404 for not-found and 422 for validation, with one consistent body schema across all endpoints. If the field-level array shape is the real one, document it everywhere and replace the generic {"error":"Bad Request"} examples.
5. Two core integration code examples are empty (critical)
Location: /reference/integrate-with-custom-checkout-and-payment-iframe
Problem: On the custom-checkout/iframe page, the JavaScript block that "defines the checkout" (using checkoutId and integrity) and the HTML block for the iframe form_action both render as empty fenced code blocks. The page also references "this customisation guide", "the advanced options guide", "demo checkouts", "Demo checkouts on GitHub", "Apple Pay options for custom checkout", and "Google Pay options for custom checkout" — none of which are linked.
Consequence: The single page that explains how to embed payments has no copyable code for its two most important steps. There is nothing for a developer to paste and nothing for an agent to extract; the custom-checkout path is undoable from the docs alone.
The fix: Populate both code blocks with working, copy-paste-complete examples (including the checkoutId/integrity wiring and the iframe form_action markup), and turn the unlinked prose references into real links.
6. Version numbers contradict each other three ways (significant)
Location: Docs home (v0.4.0), the OpenAPI specs (version: 0.2.0), /reference/billpro-api (prose "BillPro API v1")
Problem: The version selector reads v0.4.0, every OpenAPI info.version is 0.2.0, and the Get-started prose says "This documentation applies to BillPro API v1. All minor versions retain full backward compatibility." Three different version identifiers for the same API.
Consequence: Developers cannot tell which version they are reading, whether v1 in the base path (/api/v1/) relates to 0.2.0/0.4.0, or what "backward compatibility" applies to. Version pinning and changelog reasoning become guesswork.
The fix: Reconcile to a single scheme. Keep the URL path version (v1) distinct from the spec/doc version, and make info.version in the OpenAPI match the site selector.
7. create_checkout is marked required but documented with a default (significant)
Location: /reference/create-single-order
Problem: In the order schema, create_checkout is listed under required alongside currency and customer_id, yet its description says: "If true, create a checkout... Defaults to false." A required field cannot have a default — if it must always be sent, the default is meaningless; if it has a default, it is optional.
Consequence: Developers and codegen tools get contradictory signals: validators may reject requests that omit it (because it's required), while developers omit it expecting the documented default. Agents cannot resolve which behavior is authoritative.
The fix: Decide whether create_checkout is optional (remove it from required, keep the default) or required (remove the "Defaults to false" language). Document the chosen behavior.
8. date_of_birth example contradicts its own response and stated rule (significant)
Location: /reference/create-customer
Problem: The schema states: "Format DD/MM/YYYY. If invalid, the API will set it to null." The "Create customer example" sends "date_of_birth": "20/05/1972" — a valid DD/MM/YYYY date — but the documented "Create customer response" returns "date_of_birth": null.
Consequence: A developer copying the canonical example sends a valid birth date and the docs show it being silently dropped, implying the field never persists. This undermines trust in the whole example payload and hides whether DOB is actually stored.
The fix: Either correct the example response to echo "date_of_birth": "20/05/1972", or, if the API really nulls it, explain why and stop presenting a valid value as the example input.
9. Two core API reference pages document no error responses at all (significant)
Location: /reference/products-api, /reference/scheduled-payments-api
Problem: Both pages are part of the API reference, yet neither documents any error codes or error responses. Products is a conceptual overview (the API/UI split-payment gap and the id-vs-product_id trap, but no failure behavior); Scheduled Payments documents the happy-path mechanics of adjusting billing cycles but no error responses. Meanwhile the order endpoints clearly return 422s, so failures for these resources exist but are never specified here.
Consequence: Error handling for two core resources is left entirely unspecified. A developer has no documented way to know how a failed product lookup or a rejected scheduled-payment change is reported, and must reverse-engineer it from live traffic.
The fix: Add error-code/response sections to both reference pages (status codes, body shapes, and the conditions that trigger them), consistent with the corrected error contract in Issue 4.
10. Webhook documentation defers entirely off-site, and there is no Stripe webhook (significant)
Location: /reference/billpro-payment-gateway-webhook
Problem: BillPro's only webhook page defers to an external domain (cardcorp.docs.oppwa.com) for "the basic payload format, retries, decryption, and processing peak loads." No payload schema, signature/verification details, or examples live on BillPro's own site. It also states: "No webhook is available when using BillPro with Stripe," and "To find out more about how to use the webhook to get the results of repeated payments, please contact BillPro Customer Service."
Consequence: Stripe merchants have no event-driven way to learn payment outcomes, and CardCorp merchants must leave BillPro's docs (and contact support) to integrate webhooks or get repeated-payment results. There is no self-serve, verifiable webhook contract — critical for payment reconciliation.
The fix: Document the webhook payload, signature verification, and retry behavior on BillPro's own page (even if it mirrors the gateway), and state clearly how Stripe merchants should obtain payment results (e.g. polling get-order) since no webhook exists for them.
11. "Create a subscription with a trial" links to a non-existent /recipes path (significant)
Location: /reference/scheduled-payments-api
Problem: The page links "Create a subscription with a trial" to https://docs.bill.pro/recipes. There is no /recipes entry in llms.txt; the actual page is /reference/create-a-subscription-with-a-trial.
Consequence: A documented tutorial for a common subscription pattern is unreachable from the page that references it. Agents indexing from this page miss the trial recipe entirely.
The fix: Repoint the link to /reference/create-a-subscription-with-a-trial.
12. The full order-status enum is never represented in the API (significant)
Location: /docs/order-status vs /reference/get-order
Problem: The authoritative status reference defines ten states (Draft, Rejected, Pending, Cancelled, Paused, Active, Failed, Review, Suspended, Complete), but the API examples only ever show pending, active, and complete, and no API field documents the full enum.
Consequence: A developer parsing status from get-order cannot know the complete set of possible values, so client-side state handling will be built around three states and break when an order goes Suspended, Failed, Review, etc. Agents have no machine-readable enum to validate against.
The fix: Add the full status enum to the order schema in the OpenAPI spec, matching the values in /docs/order-status exactly.
13. The onboarding video demonstrates an auto-suspend feature that doesn't exist (significant)
Location: /docs/activate-billpro-with-stripe
Problem: The page closes with: "The auto-suspend option shown in the video is not available in the current version of BillPro with Stripe. Please contact BillPro Customer Support for more information." The official onboarding video therefore demonstrates a feature the current Stripe product does not have, with no in-docs explanation of what to do instead.
Consequence: A merchant or developer following the onboarding video will look for an auto-suspend setting that isn't there, conclude they've misconfigured something, and have to contact support. Onboarding material actively teaches a non-existent feature.
The fix: Re-record or annotate the video, and document the actual suspend behavior for Stripe (the Suspended order state exists in /docs/order-status — explain how it is reached without the missing option, or state plainly that auto-suspend is unavailable on Stripe).
14. The Stripe integration is a systematically under-documented path (significant)
Location: /reference/billpro-payment-gateway-webhook, /docs/activate-billpro-with-stripe, /docs/declined-transactions
Problem: Across three independent pages, Stripe is consistently the lesser-documented of the two supported gateways: "No webhook is available when using BillPro with Stripe" (webhook page); "The auto-suspend option... is not available in the current version of BillPro with Stripe" (Stripe activation page); and the entire decline-reason reference points to the CardCorp/OPPWA gateway docs (cardcorp.docs.oppwa.com/reference/resultCodes, result code 000.000.000 for approvals) with no Stripe equivalent documented anywhere.
Consequence: Stripe merchants repeatedly hit "not available," "contact support," or "see the gateway's docs" walls — for event delivery, for order suspension, and for interpreting declines. The docs present Stripe as a first-class option at signup but cannot support a Stripe integration end-to-end, which a developer only discovers feature by feature.
The fix: Add a Stripe-specific reference covering decline/result codes, payment-outcome retrieval without webhooks, and suspension behavior — or state up front, per feature, which capabilities are CardCorp-only so Stripe adopters can plan around them.
15. The "PCI DSS Level 1 compliance" callout attributes the gateway's certification but never states the merchant's own obligations (significant)
Location: /docs/secure-checkout
Problem: A 👍 (success-styled) callout headlined "PCI DSS Level 1 compliance" reads in full: "The payment gateway has PCI DSS Level 1 compliance. BillPro does not process or store sensitive card data." The certification described belongs to the payment gateway — but the reassuring headline presents "PCI DSS Level 1 compliance" as a property of the BillPro integration, and the docs never address the merchant's own PCI/SAQ scope when embedding the iframe.
Consequence: A merchant skimming the headline can reasonably conclude that integrating BillPro makes them PCI DSS Level 1 compliant, when the certification is the gateway's and the merchant still has their own (reduced, but non-zero) compliance responsibilities. Misjudging PCI scope has real audit and liability consequences for a payments integrator.
The fix: Reword the callout so it's unambiguous whose certification this is, and add a short section on the merchant's own PCI/SAQ responsibilities when using the hosted iframe.
16. Subscription billing-cycle changes are invisible in the order view (significant)
Location: /reference/scheduled-payments-api
Problem: When adjusting a subscription's billing cycle (setting scheduled_interval and scheduled_duration_unit on the last scheduled payment), the page warns: "Note that BillPro does not display these changes in the order."
Consequence: A developer who changes a subscription's schedule through the API has no way to confirm the change in the order view, and QA or reconciliation against the UI will appear to show the change never took effect. An agent verifying its own write by reading the order back will get a misleading result.
The fix: Surface the updated schedule in the order view, or document precisely which field/endpoint reflects the change so developers can verify it programmatically.
17. Create-order success example contradicts its documented status and contains stray characters (minor)
Location: /reference/create-single-order
Problem: The endpoint documents only a 200 success response, but the example body contains "status": 201. The same example value ends with a stray § character: ...PWQSeG3fL\", ... },\n "status": 201\n}§.
Consequence: Developers can't tell whether order creation returns 200 or 201 (create-customer uses 201, deepening the confusion), and the stray § will break anyone who copies the example as literal JSON or feeds it to a parser.
The fix: Make the documented HTTP status and the in-body status agree, align create-order and create-customer success codes, and remove the §.
18. Banner promises an llms index that partly 404s (minor)
Location: Docs home / llms.txt
Problem: The site banner says "visit https://docs.bill.pro/llms.txt for an index of all pages formatted in Markdown and endpoints in OpenAPI," and the Welcome page references the index, but llms-full.txt returns 404 — only llms.txt exists.
Consequence: Agents that follow the common llms-full.txt convention (full concatenated content) hit a dead end and fall back to crawling, partly defeating the purpose of advertising an AI index.
The fix: Either publish llms-full.txt or stop implying a full index exists; keep the banner consistent with what's actually served.
19. Shipping-address description is malformed and self-contradictory (minor)
Location: /reference/create-customer
Problem: The shipping_address description reads: "Optional shipping address. If sent, all parameters are required except the state. . This attribute is not validated" — note the doubled period and the contradiction between "all parameters are required" and "not validated."
Consequence: A developer cannot tell whether omitting a sub-field will be rejected ("required") or ignored ("not validated"). The malformed text signals the field isn't trustworthy.
The fix: Clarify the actual behavior (which sub-fields are mandatory when the object is present, and whether any validation occurs) and fix the punctuation.
20. Product-name typo and example-name typos (minor)
Location: /docs/welcome-to-billpro, /reference/get-order, llms.txt
Problem: The Welcome page opens "illPro is a billing automation platform..." (missing the leading "B"). The get-order spec names an example "Get Subscripton Order." The llms.txt index describes a tutorial as creating a "single pyament order."
Consequence: Individually trivial, but the product name being misspelled in the first sentence of the landing page, plus typos in spec example names and the llms.txt descriptions, erodes confidence and can mislead full-text/agent search.
The fix: Proofread the landing page, the spec example titles, and the llms.txt descriptions.
21. "BillPro URL changes" changelog entry has no dates, old URLs, or migration steps (minor)
Location: /changelog/billpro-url-changes
Problem: The only changelog entry, titled "BillPro URL changes," lists just the two current base URLs (app.bill.pro, test.bill.pro) with no prior URLs, no date, and no migration guidance.
Consequence: A developer with an older integration can't tell what changed, when, or what they need to update — the entry documents the new state but not the change, which is the entire point of a changelog.
The fix: State the previous base URLs, the cutover date, and any required action (and ensure the new live base URL also appears in the OpenAPI servers block — see Issue 1).
22. Retry counts present a default (3) and a maximum (15) without clearly distinguishing them (minor)
Location: /docs/activate-billpro-with-stripe, /docs/order-status, /docs/declined-transactions
Problem: The activation page states "you can retry it up to 15 times within 30 days" and, separately, "By default, BillPro will make 3 automatic retries, with one retry every 2 days." The order-status and declined-transactions pages repeat the "up to 15 times in 30 days" figure but not the default of 3. The relationship (3 is the default, 15 is the ceiling) is only inferable, and only if you read the activation page.
Consequence: A developer who reads only the order-status or declined-transactions page sees "15" and may model retry behavior around it, missing that the out-of-the-box default is 3 every 2 days — leading to wrong assumptions about when collection actually stops.
The fix: State the default (3 retries, every 2 days) and the maximum (up to 15 within 30 days) together in one place, with the relationship made explicit, and cross-link the other pages to it.
What they do well
- The order-status reference (/docs/order-status) is precise and internally consistent — ten clearly defined states with distinct entry conditions, including the hard-vs-soft decline distinction that drives
SuspendedvsFailed. - Operational values like the 25-minute checkout-ID expiry are stated consistently across the iframe, secure-checkout, and tutorial pages.
- The Products page flags real, easy-to-miss integration traps (the
id-vs-product_idnaming gotcha, and the split-payment order type being UI-only and unavailable in the API).
Top 3 recommendations
- Fix the machine-readable layer first: add a production
serverto every OpenAPI spec, makecustomer_idone consistent type, add the fullstatusenum, and give errors real status codes (404for not-found) with a single body schema. These break agents and generated clients silently. - Repair the entry-point and integration paths: fix the
/docs/productsand/docs/orders404 links on the Get-started page, the/recipeslink, and populate the two empty code blocks on the custom-checkout page. - Close the Stripe and error-handling gaps: document error responses on the Products and Scheduled Payments pages, give Stripe merchants a first-class reference (decline codes, payment-outcome retrieval, suspension), correct the onboarding video's missing auto-suspend feature, and reconcile the v0.4.0 / 0.2.0 / v1 version labels.