The BillPro docs cover a real billing-automation API (orders, customers, subscriptions, webhooks) layered on CardCorp/Stripe merchant accounts, and they ship an llms.txt plus embedded OpenAPI — but the surface is riddled with self-contradictions an AI agent (and a human) will silently trip over: four different API hostnames, three different version numbers, success codes that disagree with their own examples, and field names in the prose that don't match the JSON. The reference material exists; it just doesn't agree with itself.
1. The API base URL appears as four different hosts across the docs (critical)
Problem: The canonical URL changelog states only two hosts: production https://app.bill.pro/ and test https://test.bill.pro/. Yet working code samples elsewhere use two other hosts. The create-a-customer tutorial uses https://staging.bill.pro/api/v1/customers in both the curl example and the prose (step 13: "Send your request to the endpoint URL for customers at https://staging.bill.pro/api/v1/customers"). The still-live Oct 21 2024 changelog "profiles" entry uses the old host https://test.billing.gg/api/v1/customers. Meanwhile the sibling create-order tutorial uses https://test.bill.pro. That is four distinct hosts — app.bill.pro, test.bill.pro, staging.bill.pro, test.billing.gg — for the same API.
Consequence: A developer or AI agent copy-pasting the customer tutorial sends requests to staging.bill.pro, a host that appears nowhere in the canonical URL guidance and may not resolve or may not be the environment they provisioned. Agents can't use judgment to pick the "right" host; they execute whatever the page shows and fail with a DNS error or auth failure that has no obvious cause.
The fix: Pick the two canonical hosts (app.bill.pro, test.bill.pro) and global-replace every staging.bill.pro and test.billing.gg occurrence in tutorials and changelog examples. Add a CI check that greps code samples for any *.bill.pro / *.billing.gg host not in the approved set.
2. Three different version numbers describe the same product (critical)
Location: / (version selector), /reference/billpro-api, /reference/create-single-order (and every OpenAPI-bearing page)
Code verification
Runtime snippet checks
Run the public examples from this audit in an isolated sandbox and attach the pass, fixed, skipped, and failed counts to the report.
Problem: The docs site version selector reads v0.4.0. The API getting-started guide says "This documentation applies to BillPro API v1." The OpenAPI info.version embedded in every endpoint reference (e.g. create-single-order, list-customers, cancel-order) declares "version": "0.2.0". Three numbers — 0.4.0, v1, 0.2.0 — for one API.
Consequence: A developer has no reliable way to know which API version they're integrating against, or whether a breaking change applies to them. An agent ingesting the OpenAPI spec will record the API as 0.2.0 while the prose promises "v1 … full backward compatibility," and any version-pinning logic becomes meaningless.
The fix: Define one canonical version scheme and make the docs version, the prose "v1," and the OpenAPI info.version derive from a single source. If the URL path version (/api/v1/) is the contract, state that explicitly and stop surfacing 0.2.0/0.4.0 as if they describe the API.
3. Success status code contradicts its own example (200 vs 201) (critical)
Problem: The create-order tutorial step 9 says "Check the response code, which should be 200 for a successful request." But the example response body on the same page ends with "status": 201. The create-single-order OpenAPI repeats this: the response example is filed under the 200 block, yet its body literally ends "status": 201. The getting-started guide further muddies it by saying success is "200 OK or 201 Created" without saying which endpoint returns which.
Consequence: A developer who writes if (status === 200) — exactly as the prose instructs — will treat every successful order creation as a failure, because the API returns 201. This is a silent, high-impact bug for anyone branching on the documented code.
The fix: State the actual returned status per endpoint (apparently 201 for order creation) and correct step 9. Make the OpenAPI response example live under the status code it actually returns, not under 200.
4. Every documented 422 returns a body that says "Bad Request" (significant)
Problem: Across endpoints, the 422 response example is a body reading {"error":"Bad Request"} — a 400-style phrase served under a 422 status — and the example labels don't match the payload. create-single-order labels it "Missing or not found product ID," list-customers labels it "Customer page not found," cancel-order labels it "Order does not exist," yet all three return the identical generic {"error":"Bad Request"}. The getting-started guide separately promises that 422 responses carry a useful message like {"phone":["is invalid"]}.
Consequence: Developers can't programmatically distinguish a missing product from a bad page number from a nonexistent order — every error is the same string. The promised field-level error detail ({"phone":["is invalid"]}) never appears in any reference example, so error handling can't be built from the docs.
The fix: Document the real, distinct 422 bodies for each failure case (or correct the examples if the API genuinely returns a uniform body). If the body says "Bad Request," either change the status to 400 or change the message to reflect a 422 validation error.
5. Prose field names don't match the JSON (createCheckout, email_address) (critical)
Problem: In the create-order tutorial, the prose instructs "set createCheckout to true" (camelCase) while the actual JSON field — shown in the same example and in the OpenAPI required list — is create_checkout (snake_case). In the create-customer tutorial, step 7 says "You must enter an email_address," but the required field in the example body and schema is email.
Consequence: A developer following the prose verbatim sends createCheckout or email_address, which the API ignores or rejects. Because create_checkout is a required field, the order request fails validation; an agent that reads the step text rather than the JSON block will generate a body that can't succeed.
The fix: Make the prose use the exact wire field names (create_checkout, email). Ideally generate field references from the OpenAPI schema so prose and spec can't drift.
6. The state address field is typed as a status enum (significant)
Problem: In the customer address object, the state property — a geographic state/region, with examples "state": "Avon" and null — is typed {"type":"string","enum":["dormant","active"]}. Those enum values are status values, not place names, and contradict the field's own examples.
Consequence: Any consumer that validates against this OpenAPI schema will reject every real address (because "Avon" is not in ["dormant","active"]). Code generators will emit a bogus State enum type with two nonsensical members, breaking generated clients.
The fix: Remove the enum from state (it should be a free-form string), and audit whether the ["dormant","active"] enum was meant for a different status field that's now missing it.
7. OpenAPI servers URL format is inconsistent across endpoints (significant)
Location: /reference/cancel-order vs /reference/create-single-order, /reference/list-customers
Problem: Within the same API spec, cancel-order declares "servers":[{"url":"https://{env}.bill.pro/api/v1"}] (a templated {env} variable), while create-single-order and list-customers hardcode "servers":[{"url":"https://test.bill.pro/api/v1"}]. The same API advertises a different server URL shape per endpoint, and the {env} template has no documented variable definition or default.
Consequence: A tool that stitches these specs into one client gets conflicting servers entries; the templated {env} with no variables block is invalid/ambiguous and will fail OpenAPI tooling that expects a server variable definition. Developers can't tell whether the base URL is parameterized or fixed.
The fix: Use one consistent servers declaration across all endpoints. If you want a templated host, define it properly with an OpenAPI variables object listing allowed env values (test, app) and a default.
8. Error response field name flips between error and errors (significant)
Problem: On the same endpoint, the 400 and 422 bodies use a singular "error" key ({"error":"Order is already cancelled", ...}), while the 401 body uses a plural "errors" key plus an extra status field ({"errors":"Not Authorized","status":401}). The shape of the error envelope changes depending on which failure occurred.
Consequence: A developer writing one error handler can't reliably read the message — they must check both error and errors, and status is present on some errors but not others. Agents generating error-handling code will key off whichever example they saw and silently miss the other shape.
The fix: Standardize one error envelope (single key name, consistent presence of status) across all status codes, and update every example to match.
9. Subscription "list scheduled payments" curl is broken and can't run as written (significant)
Without the trailing \, the shell terminates the curl command at the URL line and treats --header ... as a separate (failing) command.
Consequence: A developer copy-pasting this block gets a curl invocation missing its auth header — at best a 401, at worst a confusing shell error. Agents that execute the snippet verbatim fail.
The fix: Add the missing trailing backslash after the --url line. Add a lint step that validates every multi-line shell sample parses as a single command.
10. Order body shape silently differs between order types (order_product vs order_products) (significant)
Location: /reference/create-a-subscription-with-a-trial vs /reference/how-to-create-an-order-with-the-billpro-api
Problem: The single-payment order example uses a plural array: "order_products": [ { "product_id": 7 } ]. The subscription order example uses a singular object: "order_product": { "product_id": 156 }. The difference between an array field and an object field is never called out.
Consequence: A developer who learned the order shape from the single-payment tutorial will send order_products (array) to the subscription endpoint and get a validation failure, or vice versa. This is exactly the kind of undocumented per-endpoint shape difference that breaks agents, which generalize the first shape they see.
The fix: Either unify the field across order types or explicitly document, in both tutorials, that subscription orders take a single order_product object while single/instalment orders take an order_products array.
11. Custom-checkout integration ships two empty code blocks (significant)
Problem: The page tells you to "use JavaScript to define the checkout" and to set the form's form_action, but the JavaScript "define the checkout" block and the HTML "form_action" block both render as empty code fences (javascript`` and html`` with no code inside). The critical example code is missing.
Consequence: This is the CardCorp custom-checkout path — the integration literally cannot be completed from the docs, because the two snippets that would show how to embed the iframe and wire the form contain nothing. There's no copy-paste source to start from.
The fix: Populate the empty fences with the actual JavaScript checkout-definition snippet (using checkoutId and integrity) and the HTML form_action markup. Add a docs check that flags empty code blocks before publish.
12. Webhook payload, retries, and repeated-payment results are not documented — they're delegated (significant)
Problem: BillPro's webhook reference documents no payload schema. It defers entirely to a third-party URL — "Read the documentation about integrating with the payment gateway's webhook at https://cardcorp.docs.oppwa.com/ … you will find important information about the webhook, such as the basic payload format, retries, decryption." And to get the results of a repeated (retried) payment, the docs say only "please contact BillPro Customer Service." Separately, the page states "No webhook is available when using BillPro with Stripe."
Consequence: A developer integrating webhooks must leave BillPro's docs entirely to learn the payload format and decryption, and there is no documented way to programmatically obtain repeated-payment results — the answer is a support ticket. Stripe-based merchants get no webhook at all, a capability gap that isn't surfaced on the Stripe onboarding pages. Reconciliation (the whole point of a billing platform) is undocumented.
The fix: Document at least the BillPro-relevant webhook payload fields (type, paymentType, result.code) inline with a concrete example, and document how to retrieve repeated-payment results via the API instead of via Customer Service. Cross-link the Stripe-no-webhook limitation from the Stripe onboarding pages.
13. Date formats are inconsistent within a single workflow (minor)
Problem: Request examples use DD/MM/YYYY — "date_of_birth": "20/05/1972" and "start_date": "17/07/2024 14:00" — while API responses and other fields use ISO 8601 (scheduled_at: "2024-07-19T14:00:00.000Z", and the create-customer response returns ISO YYYY-MM-DD). The subscription example also shows "start_date_unix": "" with no explanation of the field or its relationship to start_date.
Consequence: Developers can't tell which date format a given field expects; 17/07/2024 is ambiguous to any consumer expecting ISO and unparseable by standard date libraries. The empty, unexplained start_date_unix invites guesswork.
The fix: Standardize on ISO 8601 for all date inputs (or document the exact accepted format per field), and either document start_date_unix or remove it from the example.
14. Stray rendering artifacts and typos in reference material (minor)
Problem: The create-single-order 200 response example string ends with a stray § character ("status": 201\n}§), which makes the example invalid JSON if copied. The getting-started guide contains the typos "requeest" and "resonse," and the create-order tutorial title/description reads "single pyament order."
Consequence: The trailing § breaks copy-paste JSON parsing; an agent extracting the example body gets malformed JSON. The typos undermine confidence and can break search/keyword matching for agents indexing the page.
The fix: Remove the § artifact (likely a templating leak) and run a spell/format pass over the reference pages; the JSON examples should be machine-validated.
15. Apple Pay instruction tells you to host a certificate "at localhost" (minor)
Problem: The Apple Pay setup instruction says to "Host the certificate at localhost." Apple Pay domain verification requires the merchant certificate/domain-association file to be hosted at the publicly reachable production domain, not localhost.
Consequence: A developer who follows this literally hosts the verification file somewhere Apple can never reach, and Apple Pay domain verification fails with no clear cause.
The fix: Correct the instruction to host the domain-association file at the merchant's public domain (the .well-known path), and clarify the test-vs-production hosting requirement.
What they do well
They ship an llms.txt and embedded OpenAPI — the home page and welcome page both point agents to llms.txt for a Markdown index plus OpenAPI endpoints, which is exactly the right machine-readable entry point most docs still lack.
The order-status state machine is genuinely well specified — order-status.md defines preliminary vs live statuses precisely, and the cancel-order prose and changelog use the same status names consistently, with retry behavior (15 retries / 30 days) matching across order-status and declined-transactions.
Decline handling is mapped concretely — the Visa Category Code and MasterCard Merchant Advice Code tables tie real scheme codes to hard/soft decline behavior, which is more than most billing docs provide.
Top 3 recommendations
Fix the host and version chaos first — collapse the four API hostnames to the two canonical ones and reconcile the three version numbers (0.4.0 / v1 / 0.2.0). These are the highest-impact, most agent-hostile defects.
Make prose and OpenAPI a single source of truth — the createCheckout/create_checkout, email/email_address, 200/201, error/errors, and order_product/order_products mismatches all stem from prose drifting from the schema. Generate field/status references from the spec and validate every JSON/curl sample in CI.
Document the error and webhook contracts — replace the uniform {"error":"Bad Request"} 422 bodies with real per-case errors, fix the state enum, and bring the webhook payload (and repeated-payment results) into the docs instead of deferring to a third party and Customer Service.