The docs cover a real billing-automation REST API (customers, products, orders, scheduled payments, webhooks), but they are riddled with version/identifier contradictions, a missing production API base URL, an error model that returns "Bad Request" with a 422 for everything (including not-found) under two different body shapes, fragmented status vocabularies, and four dead cross-links — the kind of issues that break automated agents silently and strand human developers at the go-live step.
1. No production API base URL exists anywhere in the docs (critical)
Location: /reference/billpro-api.md, /changelog/billpro-url-changes.md, every OpenAPI reference page
Problem: Every documented server is the test environment, https://test.bill.pro/api/v1. The getting-started page documents only that base URL, and every reference spec hardcodes "servers": [ { "url": "https://test.bill.pro/api/v1" } ] — with one exception: update-scheduled-payment templates https://{env}.bill.pro/api/v1 (see Finding 13), which is unexplained and is the closest the docs come to revealing a production host. The changelog announces a production base URL, but only for the app: "The BillPro App and API are now available with the base URL of https://app.bill.pro/." It gives no production API base path (e.g. https://app.bill.pro/api/v1), no old→new mapping, and no dates.
Consequence: A developer can integrate and test, then has nothing to copy when they go live. They must guess whether the production API is app.bill.pro/api/v1, bill.pro/api/v1, or something else. An AI agent reading these docs will emit code that hits the test server in production and silently never charge a real card.
The fix: Publish the production API base URL explicitly on the getting-started page, add it as a second servers entry in every OpenAPI spec, and give the changelog a concrete old→new URL table that includes the API path.
2. customer_id is an integer on the way in and an alphanumeric string on the way out (critical)
Problem: The create-order request types customer_id as { "type": "integer", "description": "System identifier for the customer", "format": "int32" }, but the success response for the same call returns "customer_id": "DC19894EB7" — an alphanumeric string — while nesting the integer system id under customer.id. The orders-api page confirms this is intentional: "set the order's to the system customer . The order will return the merchant's customer identifier as the ."
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.
Consequence: The same field name carries two different types and two different meanings between request and response. A strongly-typed client or an agent that round-trips the response value back into a new request will send a string where an integer is required and get rejected, with no doc explaining why.
The fix: Use distinct field names for the two identifiers (e.g. system_customer_id integer vs merchant_customer_id string), or at minimum type the response field as a string and document the request/response asymmetry in one place next to both schemas.
3. A 422 with body {"error": "Bad Request"} is used for missing resources, the error body shape is non-deterministic, and no 404 is documented (critical)
Problem: Across endpoints, the documented failure body for not-found cases is { "error": "Bad Request" } returned with HTTP 422 — labelled "Missing or not found product ID" (create order), "Order ID not found" (get order), and "Order ID or scheduled payment ID not found" (update scheduled payment). No 404, 400, or 401 response is documented for any endpoint. Worse, the error body's shape changes within a single page: update-scheduled-payment documents both { "error": "Bad Request" } (singular key, string value) for not-found and { "errors": [ "Invalid status" ] } (plural key, array value) for invalid status. Meanwhile the order tutorial states the contract as "If there is an error in the body object, the response code will be 422" — and a 422 returning the literal message "Bad Request" contradicts that framing.
Consequence: Developers cannot distinguish a malformed request from a nonexistent order from an auth failure — all collapse into one opaque 422 with a misleading "Bad Request" string. And because the body is sometimes {error: string} and sometimes {errors: [string]}, a client cannot write a single error parser: code that reads body.error breaks on the array variant and vice versa. Error-handling code and agent retry logic have nothing reliable to branch on.
The fix: Document the full status-code set (400, 401, 404, 422) per endpoint, return 404 for missing resources, and standardize on one error body shape carrying a stable machine-readable code/field rather than a generic "Bad Request" string and an ad-hoc errors array.
4. Three different version numbers for the same API (significant)
Location: site header (v0.4.0), /reference/billpro-api.md ("API v1"), every OpenAPI info block (0.2.0)
Problem: The site header badge reads v0.4.0. The API guide states "This documentation applies to BillPro API v1." Every embedded OpenAPI spec is stamped "info": { "title": "billpro-api", "version": "0.2.0" }. Three artifacts, three answers.
Consequence: A developer cannot tell which version they are coding against, and an agent ingesting the OpenAPI spec will record 0.2.0 as the API version while the URL path says v1 and the docs site advertises 0.4.0. Version pinning and changelog reasoning become impossible.
The fix: Reconcile the version sources. Decide whether the API contract is v1 (the URL path) and make the OpenAPI info.version and the site badge reflect the same scheme, or clearly separate "docs site version" from "API version" with a labeled note.
5. Success status code disagrees with itself: docs say 200, examples say 201 (significant)
Problem: The create-single-order spec declares the success response "200": { "description": "OK" }, but the example body it ships ends with "status": 201 — and that body also contains a stray garbage character: ... "status": 201 }§. The tutorial compounds it: step 9 says "Check the response code, which should be 200 for a successful request," then shows an example whose body contains "status": 201.
Consequence: Code that checks if (status === 200) will treat a successful order creation as a failure (or vice versa). The trailing § means anyone copy-pasting the example as JSON gets a parse error.
The fix: Pick the actual success code (the example suggests 201), make the OpenAPI response, the embedded example, and the tutorial all agree, and remove the § character from the example body.
6. Recurring-interval is modeled with conflicting enums and a sprawl of field names (significant)
Problem: The create-subscription request uses recurring_collection_interval with enum [ "MONTH", "QUARTER", "YEAR" ]. The scheduled-payments and update-scheduled-payment docs use scheduled_duration_unit with enum [ "DAY", "WEEK", "MONTH", "YEAR" ]. QUARTER exists in one vocabulary and not the other; DAY/WEEK exist in the other and not the first — and the two are never reconciled. On top of that, the subscription response introduces yet more names for the same concept: scheduled_unit, scheduled_interval, scheduled_duration, and get-order returns scheduled_duration_unit / scheduled_duration_number.
Consequence: A developer setting up a quarterly subscription cannot tell whether to send QUARTER or scheduled_duration_unit: MONTH with interval 3, and cannot map the response fields back to the request fields. Agents have no way to learn the canonical schema.
The fix: Standardize on one field name and one enum for billing interval across request, response, and the scheduled-payments API, and document how (or whether) QUARTER maps to MONTH × 3.
7. Scheduled-payment status model is fragmented: a 3-value writable enum vs a 9-name prose vocabulary, with no canonical list (significant)
Location: /reference/update-scheduled-payment.md
Problem: On a single page, the writable status field is typed "enum": [ "cancelled", "skipped", "scheduled" ], but the same page's prose references a much larger status vocabulary: payments can have a status of scheduled, soft-declined, hard-declined, complete, review, merged, recovered, skipped, or cancelled. There is no single place defining what each scheduled-payment status means, which are settable, and which are observed-only — and this is a third distinct status vocabulary, separate from both the preliminary/live order statuses (see Finding 10) and the order-create enums.
Consequence: A developer reading the status enum sees only three possible values, while the prose elsewhere references nine, with no mapping between "statuses a payment can have" and "statuses you can set it to." Building a correct scheduled-payment state machine — or an agent that reasons about payment lifecycle — is guesswork.
The fix: Publish one canonical scheduled-payment status reference (like the order-status page) that lists every status, its meaning, and whether it is settable via the API, and make the status enum reference it.
8. Schema examples leak a stale, different domain (test.billing.gg) (significant)
Problem: Live examples use test.bill.pro, but the schema example values point at a different domain: the subscription payment_link schema example is https://test.billing.gg/checkout/610/... and the update-scheduled-payment response schema url example is https://test.billing.gg/api/v1/orders/610/scheduled_payments.json?id=21341. The same pages' live examples use test.bill.pro for the identical fields.
Consequence:billing.gg appears to be a former/internal hostname. Developers and agents that read the schema example (not the live example) will build URLs against a domain that isn't the documented platform, producing requests that fail or hit the wrong host.
The fix: Replace every billing.gg occurrence in schema examples with the canonical bill.pro host, and grep the full spec set for any other stale domains.
9. Four cross-links in the API docs are dead (significant)
Problem: Confirmed 404s linked directly from the docs: the getting-started error-code section links "Products" → /docs/products (404) and "Orders" → /docs/orders (404), while linking Customers/Scheduled Payments to working /reference/... slugs in the same sentence. The create-subscription page links "Scheduled Payments" → /reference/scheduled-payments (404; the live slug is scheduled-payments-api). The scheduled-payments overview links "Recipes" → /recipes (404) for "Create a subscription with a trial." The llms.txt index itself sends Products/Orders to /reference/... while the guide sends them to the dead /docs/... paths.
Consequence: The error-code reference and the "subscription with a trial" recipe are simply unreachable from where they're cited. Agents following the llms.txt index versus the prose links land on different (and broken) targets.
The fix: Repoint the four links to live slugs (/reference/products-api, /reference/orders-api, /reference/scheduled-payments-api), publish or remove the "Recipes" reference, and make the prose links match the llms.txt index.
10. The two order-status lists don't match — cancelled and complete are dropped (significant)
Location: /reference/orders-api.md vs /docs/order-status.md
Problem: The canonical order-status page defines Cancelled as a preliminary status and Complete as the live terminus. The orders-api overview enumerates the statuses differently: "Preliminary orders have a status of draft, pending, or rejected" (omits cancelled) and "Live orders have a status of active, failed, suspended, paused, or review" (omits complete).
Consequence: A developer building a state machine off the orders-api page will be missing two real states, including the terminal complete status, and may mishandle completed or cancelled orders.
The fix: Generate both lists from one source of truth so the preliminary/live status sets are identical everywhere they appear.
11. Customer address/country fields are accepted unvalidated, and a bad date_of_birth is silently nulled with no error (significant)
Location: /reference/create-customer.md
Problem: The create-customer schema explicitly marks multiple fields as unvalidated: country ("ISO-3166, 2-character country code. This attribute is not validated"), postal_code ("This attribute is not validated"), and shipping_address ("This attribute is not validated"). Separately, date_of_birth is typed string with "Format DD/MM/YYYY. If invalid, the API will set it to null" — invalid input is dropped silently rather than rejected.
Consequence: A billing platform that stores customer addresses and country codes without validation will accept malformed data that surfaces later in invoices, tax handling, or card-scheme compliance. And because a malformed DOB becomes null with no error returned, a developer sending a wrong-format or out-of-range date loses the value entirely without any signal — the request "succeeds" while quietly discarding data.
The fix: Validate country (against ISO-3166) and postal_code server-side, and return a validation error for a malformed date_of_birth instead of silently nulling it. At minimum, document the exact accepted DOB format and state clearly which fields are stored verbatim without checks.
12. Pagination has no metadata and no reliable end-of-list signal (significant)
Location: /reference/billpro-api.md
Problem: The getting-started page documents listing entirely through prose: "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." There is no total count, no next/has_more field, and no cursor.
Consequence: A client has to infer "more pages exist" purely from whether a page returned exactly the page size. When the total is an exact multiple of 25, the client must issue an extra request that returns an empty page just to learn it's done, and there is no way to know the total result count up front. For an AI agent, the absence of any structured pagination field means it cannot reliably plan or terminate a paged crawl.
The fix: Add structured pagination metadata to list responses (total count and/or a has_more/next_page field) and document it in the schema, rather than asking clients to infer end-of-list from the item count.
13. Critical billing default (scheduled_duration_unit defaults to WEEK) is buried, and only one endpoint templates its server URL (significant)
Location: /reference/update-scheduled-payment.md
Problem: Two issues converge on this page. First, scheduled_duration_unit "If not supplied, defaults to WEEK" — a consequential default for how often a customer's card is charged, stated only in a field description. Second, this is the only OpenAPI block whose server is templated https://{env}.bill.pro/api/v1; every other endpoint hardcodes test.bill.pro, so the docs are internally inconsistent about whether {env} is a substitutable variable.
Consequence: A developer who omits the interval unit silently gets weekly billing instead of the monthly cadence they likely intended. And the lone {env} template is the closest the docs come to revealing the production host (see Finding 1) — but it's unexplained and contradicts every other spec.
The fix: Surface the WEEK default prominently (and reconsider whether a billing-interval field should default at all rather than be required), and standardize the servers block across all specs using the documented {env} template with allowed values.
14. Retry behavior leads with the maximum and omits the default on the canonical status page (minor)
Problem: The order-status page presents only the headline figure: soft-declined payments "may be retried up to 15 times in 30 days," with no mention of the default. The Stripe activation page is the only place that states both: "you can retry it up to 15 times within 30 days" (the maximum) and "By default, BillPro will make 3 automatic retries, with one retry every 2 days after the scheduled collection date" (the default). The two figures aren't contradictory — one is the cap, one is the default — but the canonical status reference omits the default entirely.
Consequence: A developer reasoning about dunning/retry behavior off the order-status page will assume up to 15 attempts when the out-of-the-box behavior is actually 3, materially changing revenue-recovery and customer-communication assumptions.
The fix: State the default (3 every 2 days) and the maximum (up to 15 in 30 days) together on the order-status page as well as the Stripe page, and clarify which is configurable.
15. collect_initial_payment default contradicts its own example (minor)
Location: /reference/get-order.md
Problem: The schema lists "collect_initial_payment": { "type": "boolean", "example": false, "default": true }. The shown example value (false) is the opposite of the stated default (true).
Consequence: A reader skimming the example infers the default is false and omits the field expecting payment not to be collected, when the actual default collects it.
The fix: Make the example match the default, or add a one-line note explaining why the example deliberately overrides it.
16. Typos in the product name and code-example titles (minor)
Problem: The Welcome page's first body word is "illPro" (missing the capital B) on the most-visited page. The order tutorial subtitle and the llms.txt description both say "single pyament order." Subscription and get-order examples are titled "Get/Create Subscripton" (twice).
Consequence: Low functional impact, but the misspelled product name on the landing page and the repeated example-title typos make the spec look unmaintained to both readers and indexing agents.
The fix: Run a spell check across pages and example names; fix the "illPro" landing-page typo and the "Subscripton"/"pyament" example/description typos first.
17. The CardCorp webhook offloads its core spec to a third-party site, and there's no webhook at all for Stripe (minor)
Problem: The webhook reference delegates payload format, retries, decryption, and load handling to an external third-party site: "Read the documentation about integrating with the payment gateway's webhook at [https://cardcorp.docs.oppwa.com/]." The page also states "No webhook is available when using BillPro with Stripe."
Consequence: Webhook integrators must leave the BillPro docs entirely to learn how to decrypt and validate notifications, and there is no documented event mechanism for Stripe merchants — a gap that isn't surfaced until you reach this page.
The fix: Document at minimum the BillPro-specific payload fields (merchantInvoiceId, merchantCustomerId, merchantTransactionId/ZUID, result.code000.000.000 approval) inline, and add a clear note on the integration index that Stripe merchants must use Stripe's own webhooks instead.
What they do well
An llms.txt index exists and points agents to Markdown pages and OpenAPI specs — the right instinct for machine consumption, even if its links don't all match the prose.
The canonical /docs/order-status page gives precise, well-defined state definitions (Draft, Rejected, Pending, Cancelled, Paused, Active, Failed, Review, Suspended, Complete) — a solid single source of truth that the rest of the docs should defer to.
Operational honesty in spots: the Stripe page openly flags that the onboarding video shows an auto-suspend feature "not available in the current version," which is better than letting users chase a phantom feature.
Top 3 recommendations
Publish the production API base URL and add it to every OpenAPI servers block — today the only live-looking host is an unexplained {env} template on one endpoint, so customers can't reliably go to production.
Fix the API contract contradictions that break automated clients: the customer_id integer-in/string-out asymmetry, the 200-vs-201 success code, and the 422/"Bad Request"/no-404 error model — including the two different error body shapes ({error: string} vs {errors: [string]}).
Unify the duplicated vocabularies — one version number, one order-status list, one canonical scheduled-payment status reference, one billing-interval field+enum, one host (bill.pro, never billing.gg) — and repair the four dead cross-links.