The docs cover a real card-billing platform with an OpenAPI spec, tutorials, and integration guides — but they are riddled with internal contradictions (three version numbers, two domains, a body field that disagrees with the declared HTTP status) and several load-bearing gaps (no production base URL, empty integration code blocks, an error body that's always "Bad Request", a globally-required parameter documented in only one place) that will silently break any agent or developer trying to integrate from the docs alone.
1. No production API base URL is ever stated (critical)
Location: /reference/billpro-api ; OpenAPI servers in /reference/create-single-order
Problem: The getting-started page only gives the test base URL (https://test.bill.pro/api/v1/) and tells users to "Get the API base URL, which is the first part of the Api Order Creation URL." The OpenAPI servers array lists exactly one entry — the test server https://test.bill.pro/api/v1. The changelog mentions a production app at https://app.bill.pro/, but no page ever states the production API base path (presumably https://app.bill.pro/api/v1/, but it is never written).
Consequence: A developer following the docs literally ships against the test environment, because there is no documented production endpoint to point at. An AI agent generating client code from the OpenAPI spec will hardcode the test server, since that's the only servers entry. Going live requires guessing the production URL.
The fix: State the production API base URL explicitly (e.g. https://app.bill.pro/api/v1/) on the getting-started page, and add a production entry to the OpenAPI servers array alongside the test server.
2. Every 422 example returns {"error": "Bad Request"} regardless of the actual error (critical)
Problem: Each endpoint labels its 422 example with a distinct, specific failure — "Missing or not found product ID", "Customer page not found", "Order ID not found", "Missing product ID" — yet the response body in every case is identical: { "error": "Bad Request" }. The phrase "Bad Request" is the HTTP 400 reason, returned under a 422 status, with no machine-readable code or field to distinguish the conditions.
Consequence: There is no error contract. A human can't tell a missing product from a not-found order from a bad page number, and an automated client literally cannot branch on the failure — every distinct error deserializes to the same string. Retrying, surfacing a useful message, or recovering programmatically is impossible.
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.
The fix: Return distinct, documented error codes/messages per failure mode (e.g. product_not_found, customer_not_found), and make each 422 example body match its label. Also reconcile status semantics — a missing order (issue 9) is arguably a 404, not a 422 carrying a 400 phrase.
3. Critical integration code blocks are empty (critical)
Problem: The page instructs the reader to "use JavaScript to define the checkout" and to display the payment iframe, but the two code fences that should contain the JavaScript checkout definition and the HTML iframe markup are empty (```javascript / ```html with no body).
Consequence: The single most important artifact on the custom-checkout integration page — the code you copy to render the payment form — does not exist. A developer (or agent) cannot complete the CardCorp custom-checkout integration from these docs at all; there is nothing to copy.
The fix: Populate both code blocks with the actual JavaScript checkout definition (showing where checkoutId and integrity go) and the HTML iframe snippet.
4. Same payment_link field shows two domains within one endpoint after the bill.pro move (critical)
Problem: On Create Subscription Order, the success example's payment_link is https://test.bill.pro/checkout/610/... while the response schema's payment_link example on the same page is the legacy https://test.billing.gg/checkout/610/... — the same field, two domains, one endpoint. The legacy billing.gg host also survives elsewhere in copy-paste material: the Create Customer example sets web_url: "https://test.billing.gg", and the Oct 21 2024 changelog curl uses https://test.billing.gg/api/v1/customers — even though the Aug 27 2025 changelog and /changelog/billpro-url-changes announce the move to app.bill.pro / test.bill.pro.
Consequence: A developer parsing or allowlisting payment-link hosts can't tell which domain is authoritative — the schema says one thing, the example another, within the same reference page. Requests copied from older examples hit a dead/legacy host.
The fix: Make the subscription payment_link resolve to one consistent domain in both schema and example, then global find-and-replace billing.gg → bill.pro across all examples and changelog snippets.
5. customer_id is an integer on input but a string on output (critical)
Problem: Orders are created with an integer customer_id, but the response returns customer_id as a 10-character alphanumeric string such as "DC19894EB7". The Create Customer page confirms BillPro generates an "alphanumeric 10-character customer_id", which contradicts sending an integer in the order payload.
Consequence: Strongly-typed clients and generated SDKs fail to deserialize every order response (an int field receiving "DC19894EB7"), and developers who echo the created value back into a subsequent request send the wrong type. This breaks on every order call, not an edge case, which is why it belongs alongside the other production-blocking defects.
The fix: Use one consistent type and field meaning for customer_id across request and response (document the internal numeric id separately from the alphanumeric merchant customer_id), and correct the order examples accordingly.
6. Success example's body field disagrees with the declared HTTP status (significant)
Problem: The OpenAPI response and the tutorial agree the success HTTP code is 200 ("200: OK"; step 9 says "should be 200 for a successful request"). But the success example body on both pages ends with "status": 201 (plus a stray }§ glyph). So the documented HTTP status (200) and an embedded body field labelled status (201) disagree, and there is a corrupt character in the example JSON.
Consequence: It's unclear whether the success signal is the HTTP status or the status body field, and they don't match. A client that reads body.status to confirm success will key off 201, while one that checks the HTTP code keys off 200 — and the § makes the example invalid JSON if pasted directly.
The fix: Make the example body's status field match the declared HTTP status (or drop the body field if the HTTP code is the source of truth), document which is authoritative, and remove the stray § character.
7. Three conflicting version identifiers for the same API (significant)
Problem: The site header badge reads v0.4.0. The getting-started page states "This documentation applies to BillPro API v1." The OpenAPI definition declares "version": "0.2.0". Three different numbers, no explanation of how they relate.
Consequence: Developers can't tell which version they're reading about or coding against, and tooling that keys off info.version (SDK generators, changelog diffing) will report 0.2.0 while the docs claim v1. It undermines the page's own promise that "all minor versions retain full backward compatibility."
The fix: Reconcile the identifiers — if the API surface is "v1", set OpenAPI info.version accordingly (or explain that 0.2.0 is the spec revision within API v1) and clarify what the v0.4.0 site badge versions.
8. profile_id is required on all requests but documented in only one place (significant)
Problem: The changelog states plainly that for multi-profile merchants "you must include the profile_id as a query parameter with all requests." Yet this requirement appears only in the changelog — the getting-started/auth page (/reference/billpro-api), which is where developers learn how to call the API, never mentions it. Among the reference endpoints, profile_id is surfaced as a parameter only on List Customers; it does not appear in the parameter lists shown for Create Single Order or Get Order. (Those OpenAPI excerpts are truncated with ..., so confirm the parameter is genuinely absent before publishing.)
Consequence: A multi-profile merchant who follows the getting-started page and the individual endpoint references will omit profile_id and silently break every request — and per issue 2 the only feedback is the contentless {"error":"Bad Request"}, with nothing pointing at the missing parameter.
The fix: Document the profile_id requirement on the getting-started/auth page as a global rule, and add it to the parameter list of every endpoint that requires it (not just List Customers).
9. Tutorial tells you to set createCheckout, but the field is create_checkout (significant)
Problem: Step 5 says "If you have a CardCorp merchant account and you want to integrate with a custom checkout, set createCheckout to true." Everywhere else — the OpenAPI schema and the rest of the same tutorial step — the field is snake_case create_checkout.
Consequence: A developer who copies the camelCase name sends an unrecognized field; the checkout is never created and the custom-checkout integration silently fails to return checkout_id/form_action. Hard to debug because there's no error (issue 2).
The fix: Change createCheckout to create_checkout in step 5.
10. Get Order's two examples use different field names for the same data (significant)
Location: /reference/get-order
Problem: On a single endpoint, the single-payment example returns channel_id and channel_payment_method, while the subscription example returns profile_id and profile_payment_method and adds scheduled_duration_unit / scheduled_duration_number — and those scheduled_duration_* fields do not appear in the documented response schema. The labeled 422 "Order ID not found" returns {"error":"Bad Request"} under a 422 (a missing order arguably warrants 404).
Consequence: A client parsing order responses must handle two different key names for the merchant/channel identifier and payment method depending on order type, with no documentation that this varies. The scheduled_duration_* fields present in the subscription example are absent from the schema, so generated models drop them.
The fix: Standardize on one field name per concept (or document the per-order-type variation explicitly), and add the scheduled_duration_* fields that real responses return to the response schema.
11. Subscription scheduling uses at least three different field-name sets (significant)
Problem: The subscription request enum is recurring_collection_interval with values ["MONTH","QUARTER","YEAR"]. Prose elsewhere refers to scheduled_duration_unit with values DAY/WEEK/MONTH/YEAR. The success response uses yet another set — scheduled_unit, scheduled_interval (and scheduled_duration is mentioned). Get Order adds scheduled_duration_unit/scheduled_duration_number.
Consequence: The same concept (billing cadence) is named four ways with two different value sets (QUARTER exists in one, DAY/WEEK in another). A developer cannot map request fields to response fields, and an agent has no way to know that recurring_collection_interval on input becomes scheduled_unit on output.
The fix: Define one canonical schedule vocabulary, document the request→response field mapping in a table, and reconcile the allowed value lists (does the API support DAY/WEEK/QUARTER or not?).
12. Subscription uses singular order_product; single payments use plural order_products (significant)
Location: /reference/create-subscription-order vs /reference/create-single-order
Problem: Subscription orders take a single order_product object ("In subscription orders, you can only add one product"), while single-payment orders take an order_products array. The naming difference is one character and is never called out.
Consequence: Developers and agents reusing a single-payment order template for a subscription (or vice-versa) will send the wrong shape — an array where an object is expected — and get the contentless {"error":"Bad Request"} (issue 2), with nothing pointing at the field-name/structure mismatch.
The fix: Explicitly document both field names side by side, note that subscriptions accept exactly one product, and make sure the 422 error names the offending field.
13. 000.000.000 approval code is unqualified on the webhook page but live-environment-only on the declines page (significant)
Location: /reference/billpro-payment-gateway-webhook vs /docs/declined-transactions
Problem: The webhook page states without qualification: "If the value of result.code is 000.000.000, the payment is approved. Otherwise, it is declined." The Declined Transactions page says the result code is 000.000.000 for an approved transaction "in the live environment", and that "Other values mean the transaction was declined (or you are using a test environment)" — i.e. in test the same 000.000.000 mapping does not hold.
Consequence: A developer wiring up webhook handling against the test environment (the only environment with a documented base URL — issue 1) will follow the webhook page's unconditional rule and misclassify outcomes, because the test-vs-live caveat lives on a different page. Approvals and declines get mislabeled silently.
The fix: Add the live-vs-test qualifier to the webhook page's result.code rule, and cross-link to the declined-transactions environment caveat so both pages state the same condition.
14. Soft-decline retry default (3) vs maximum (15) is never explained (significant)
Problem: Three pages repeat that soft declines "may be retried up to 15 times in 30 days." /docs/auto-retry-billing-events says automatic retries occur "on days 1, 2, and 3 after the scheduled collection date" by default and that the days "you can configure," then notes "When BillPro has consumed all the automatic retries, it will deactivate the auto-retry functionality." The pages never state that 3 (days 1–3) is the configurable default and 15-in-30-days is the ceiling — the relationship between the two numbers is left implicit.
Consequence: Merchants planning dunning logic see "15 times" on three pages and a "days 1, 2, 3" schedule on a fourth, with nothing connecting them. They can't tell how many automatic attempts they actually get by default, or how to raise it toward 15, leading to wrong assumptions about when an order moves to Failed/Suspended.
The fix: State the relationship explicitly on all four pages — e.g. "3 automatic retries by default (days 1–3), configurable up to a maximum of 15 within 30 days, plus unlimited manual retries" — and show how to configure the retry days.
15. Stripe merchants have no documented push/notification mechanism (significant)
Problem: The only async result mechanism documented is the payment-gateway webhook, which the page itself scopes to CardCorp: "No webhook is available when using BillPro with Stripe." Setup is also gated behind "BillPro Customer Service configures the webhook," and the payload details live entirely on the external cardcorp.docs.oppwa.com site.
Consequence: Stripe-backed merchants — a first-class acquirer per the welcome page — are left with only polling Get Order to learn payment outcomes, with no documented latency, polling cadence, or alternative. The webhook page sits in the general API reference, so it reads as universally applicable until the one-line caveat.
The fix: Document the supported outcome-notification path for Stripe merchants (Stripe webhooks, polling guidance with recommended intervals, or a BillPro-native callback), and clearly label the gateway webhook as CardCorp-only at the top of the page.
16. addresses[].state is typed with an account-status enum (significant)
Location: /reference/list-customers
Problem: In the customer addresses schema, the state property is declared { "type": "string", "enum": ["dormant", "active"] } — an account-lifecycle enum — even though the example values are geographic ("Avon", null).
Consequence: Any client validating against this schema will reject every real address ("Avon" isn't in ["dormant","active"]), and code generators will produce a bogus two-value enum type for a free-text region field. The example data directly contradicts the declared schema.
The fix: Remove the enum and type state as a plain (nullable) string for the address region.
17. List/pagination has no metadata, forcing guess-and-check iteration (significant)
Problem: The docs state "The default page size is 25 items in a list. There is no pagination metadata. Get the pages sequentially by incrementing the page number. If the page list contains 25 items, try to get the next page, until you get a page with fewer than 25 items." page is required: true on List Customers.
Consequence: With no total count, no next/has_more, and no cursor, clients must always issue one extra request to discover they've reached the end, and cannot show totals or parallelize fetching. For agents this means an unbounded "keep fetching until short page" loop with no termination guarantee if a page happens to return exactly 25 of a final set.
The fix: Return pagination metadata (total count and/or has_more/next-page indicator). At minimum, document the exact terminal condition and whether page can be omitted.
18. Payment-links page recommends iframe embedding with no 3DS/embedding caveats (minor)
Location: /reference/integrate-with-payment-links
Problem: The page tells merchants to "create an iframe on your website and set the source to the payment link URL," which opens the "BillPro 3D secure checkout." It gives no warning that 3DS/issuer pages frequently set X-Frame-Options/frame-ancestors policies that block iframing, and — unlike the custom-checkout page — provides no link expiry, error-handling, or result-detection guidance.
Consequence: This is the Stripe-compatible integration path, yet a merchant who follows it may ship an iframe that issuer 3DS step-up pages refuse to render, with no documented way to detect completion or handle failures. The integration appears to work until a real 3DS challenge breaks it in production.
The fix: Note that 3DS/issuer pages may block iframe embedding (recommend a redirect or popup fallback), and document link expiry, error handling, and how to detect a completed/failed payment from the payment-link flow.
19. "Host the certificate at localhost" and inconsistent go-live URLs (minor)
Problem: The Apple Pay setup instruction says to "Host the certificate at localhost." The go-live table also mixes URL formats: gateway test is https://eu-test.oppwa.com (with scheme) while live is eu-prod.oppwa.com (no scheme), and BillPro live is given as https://app.bill.pro (no /api/v1 path, unlike the documented API base).
Consequence: "localhost" cannot serve a production Apple Pay domain-association certificate, so the instruction is misleading for go-live. The scheme/path inconsistencies invite copy-paste errors (missing https://, wrong base path).
The fix: Correct the Apple Pay hosting instruction to a real public domain path (e.g. /.well-known/apple-developer-merchantid-domain-association), and normalize every URL in the table to include scheme and the correct path.
20. Trial tutorial mislabels a curl block as JSON and mixes date formats (minor)
Problem: The first code block is fenced as ```json but contains a curl command. The flow also uses empty start_date_unix: "" and <> placeholders for path IDs/tokens, and mixes date formats: the request start_date is 17/07/2024 14:00 (DD/MM/YYYY HH:MM) while the scheduled-payment update uses ISO 2024-07-19T14:00:00.000Z.
Consequence: Syntax highlighting and "copy as JSON" tooling break on the mislabeled block, and a developer can't tell which date format each field expects, risking rejected or misparsed dates. There is no first-class trial field — the trial is faked via create-order → list scheduled payments → PUT next billing date — which should be stated up front.
The fix: Tag the block as ```bash/shell, standardize on one documented date format per field (or document both explicitly), mark <> placeholders clearly, and state at the top that trials are implemented by editing the next scheduled payment.
21. Silent data coercion and "not validated" fields are under-warned (minor)
Location: /reference/create-customer
Problem:date_of_birth "If invalid, the API will set it to null"; country ("ISO-3166, 2-character country code. This attribute is not validated"), postal_code, and shipping_address are all "not validated." The shipping-address description also contains a doubled period: "This attribute is not validated. ."
Consequence: A typo'd birth date is silently discarded (no error), and unvalidated country/postal fields let malformed data into records that downstream tax/shipping logic may depend on — failures surface far from the cause.
The fix: Either validate these fields and return an error, or prominently flag in one place that they are stored as-is and that invalid date_of_birth is silently nulled. Fix the doubled period.
22. Pervasive typos and a corrupted heading in core pages (minor)
Problem: The Welcome page opens with a corrupted word — "illPro is a billing automation platform" (missing leading "B"). Other instances include "requeest"/"resonse" on the getting-started page, "single pyament order" in the order tutorial title, and "Subscripton Order" on Get Order.
Consequence: Individually trivial, but a corrupted product name in the very first sentence and typos in tutorial titles undercut trust and can break exact-match search/indexing (including the llms.txt-driven agent indexing the docs advertise).
The fix: Proofread the core pages; fix "illPro" → "BillPro", "requeest", "resonse", "pyament", and "Subscripton".
What they do well
They ship an llms.txt index and an OpenAPI definition, and explicitly invite AI agents to use them — the right instincts for machine consumption, even if the spec content needs cleanup.
The order-status and decline-handling pages (live statuses, soft vs hard declines, Visa Category Codes / MasterCard Merchant Advice Codes) are genuinely thorough operational references.
Auth is straightforward and clearly explained (Authorization: Bearer <token>, admin-user token generation steps).
Top 3 recommendations
Establish an error contract. Stop returning {"error":"Bad Request"} for every 422; return distinct codes/messages per failure and align status codes (404 for missing resources). This is the single biggest blocker for any automated client.
Make the spec deployable. Add the production API base URL and a production servers entry, fix the empty custom-checkout code blocks, and resolve the within-endpoint billing.gg/bill.pro split so copy-paste works end to end.
Reconcile the contradictions and surface the global rules. One version number, one customer_id type, one schedule vocabulary, create_checkout spelled consistently, the success body field matching the HTTP status, and the globally-required profile_id documented on every endpoint — these internal disagreements and buried requirements are what make the docs unsafe to follow literally.