xPay's docs are broad and agent-friendly on the surface (llms.txt, llms-full.txt, OpenAPI JSON all published), but the getting-started page contradicts the entire API reference on both authentication scheme and endpoint paths, several code examples are broken as written, and the error-handling contract disagrees with itself between pages.
1. Getting-started example uses Bearer auth, but the API is Basic auth (critical)
Location: /developer-resources/getyourkeys
Problem: The "Get Started" page (/developer-resources/getyourkeys) — the first page a developer integrating xPay is sent to — shows this usage example:
But the Authentication page (/developer-resources/authentication) says the opposite: "At xPay, we follow the Basic Auth RFC (RFC 7617) standard for authenticating requests made to our API" with Authorization: Basic <base64(publicKey:privateKey)>. Every endpoint's OpenAPI definition confirms security: - basicAuth: []. There is no Bearer token anywhere else in the docs — xPay issues a public/private key pair, not a single YOUR_API_KEY.
Consequence: A developer (or an AI agent) who copies the very first curl example in the docs sends the wrong auth scheme with a credential shape that doesn't exist (YOUR_API_KEY singular vs. a key pair) and gets a 401 — and the Errors page then compounds the confusion (see issue 4). Agents in particular will latch onto the getting-started snippet as canonical.
The fix: Rewrite the Usage Example on /developer-resources/getyourkeys to use -u "PUBLIC_KEY:PRIVATE_KEY" (or an explicit Authorization: Basic ... header) consistent with the Authentication page.
2. Getting-started example hits /v1/payments/create-intent, a path that exists nowhere in the API reference (critical)
Location: /developer-resources/getyourkeys
Problem: The same usage example calls https://api.xpaycheckout.com/v1/payments/create-intent. The Create Intent reference (/developer-resources/endpoints/payments/create-intent) and its OpenAPI spec define the path as POST /payments/create-intent — no /v1/ prefix. No other endpoint in the reference uses a /v1/ prefix either (paths are /payments/..., , , ).
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 one copy-pasteable request on the getting-started page 404s (the Errors page even says 404 means "You're likely hitting the wrong endpoint"). Combined with issue 1, both lines of the first example a developer sees are wrong.
The fix: Change the example path to /payments/create-intent and include a minimal valid JSON body (amount, currency, callbackUrl, customerDetails are all required per the schema).
3. Broken Java webhook verification example — function never returns a value (significant)
Problem: The Java example for HMAC-SHA512 verification (the "default method") is declared private boolean verifySignature(...) but its last statement discards the comparison result and the method has no return:
This does not compile ("missing return statement"). The Node.js, Python, and Rust variants all correctly return the comparison.
Consequence: Webhook signature verification is the security-critical snippet developers copy verbatim. Java integrators get a compile error at best — or, if they "fix" it by adding return true, an integration that accepts forged webhooks.
The fix: Change the last line to return incomingSignature.equals(Base64.getEncoder().encodeToString(macData)); (ideally using a constant-time comparison like MessageDigest.isEqual).
4. Idempotency page and Errors page disagree on what concurrent duplicate requests return (significant)
Location: /developer-resources/idempotency vs /developer-resources/errors
Problem: The Idempotent Requests page says: "Concurrent requests: If a duplicate request is sent while the original request is still being processed, the API will return a 400 Bad Request response indicating that the initial request is still in progress." The Errors page says the same situation returns 409 Conflict: "409 | Conflict | Multiple idempotent requests were hit concurrently" and defines a dedicated conflict error code "Returned when multiple idempotent requests are concurrently hit."
Consequence: Retry logic keyed on status codes behaves differently depending on which page the developer read. 400 is normally "don't retry, fix the request"; 409-on-concurrency is "back off and retry." An integration built from the idempotency page will misclassify a transient race as a permanent client error — for a payments API, that can mean dropped charges or unnecessary duplicate intents.
The fix: Pick one status code for the in-flight-duplicate case, document it identically on both pages, and state the recommended client behavior (retry after the first request settles).
5. Payment-result code example compares a fetch Response object to a string (significant)
Problem: The final step's JavaScript example — the code that decides whether to show the customer a success or failure page — is:
const status = await fetch(`/payment-status?xpay_intent_id=${intentId}`);
if(status === "SUCCESS") {
fetch resolves to a Response object, so status === "SUCCESS" is always false; the example never awaits .json()/.text() or reads the body. As written, every payment — including successful ones — falls into the failure branch.
Consequence: Developers who copy the canonical one-time-payments integration example ship a flow that shows a failure page after every successful payment. This is on the primary integration guide for the product's flagship flow.
The fix: Complete the example: const res = await fetch(...); const { status } = await res.json(); if (status === "SUCCESS") { ... }.
6. Rate-limit quotas mix "per minute" and "per second" in the same sentence (significant)
Location: /developer-resources/rate-limits
Problem: The Quotas section reads: "Our throttling system enforces the following request quotas per minute: Live mode: 10 requests per second, Sandbox: 10 requests per second."
Consequence: 10/second and 10/minute differ by 60x. A developer sizing batch jobs (e.g., paginated reconciliation via List Intents, which the docs themselves warn is rate-limited) can't tell whether their budget is 600 requests a minute or 10. Agents parsing the page get two contradictory units for the same quota.
The fix: Delete "per minute" from the lead-in (or restate the quota once, in one unit), and document the 429 retry guidance next to it.
7. Saved Cards guide cites a wrong endpoint path: POST /payment/create-intent (minor)
Location: /products/one-time-payments/saved-cards
Problem: Step 2 of the Developer Flow labels the API as "POST /payment/create-intent" (singular "payment"). The actual path per the API reference is /payments/create-intent. Step 1's "POST /customer/create" is correct, so the inconsistency reads as authoritative rather than a typo.
Consequence: Anyone hand-constructing the URL from this page gets a 404; agents extracting endpoint paths from prose now have three variants of the same path across the docs (/payments/create-intent, /v1/payments/create-intent, /payment/create-intent).
The fix: Correct the label to POST /payments/create-intent and audit prose-embedded paths against the OpenAPI spec.
8. Unresolved template variable leaks into the Get Intent page (minor)
Problem: The anti-polling warning renders in crawled page text as: "Avoid polling this endpoint; use webhooks for real-time {entityType_0} updates." The page's compiled MDX shows entityType_0 is a variable meant to interpolate "payment intent"; in the .md export the variable is dropped entirely, leaving "use webhooks for real-time updates" with a double space.
Consequence: Human readers see a leaked template token; LLM agents consuming the .md/llms.txt surface get a sentence with the noun missing. Either way the warning that's supposed to steer integrators toward webhooks reads as broken.
The fix: Replace the MDX variable with literal text (or fix the export pipeline so variables resolve in both HTML and Markdown surfaces).
9. "Team Roles & Permissions" page lives at the slug /untitled-page (minor)
Problem: The llms.txt index lists: "Team Roles & Permissions: What each permission on the Manage Team page controls…" — a real, linked content page shipped under a placeholder slug.
Consequence: The URL communicates "unfinished" for security-relevant content (roles and permissions), it's unguessable and hostile to search, and any future rename to a real slug will break existing links.
The fix: Move the page to a descriptive slug (e.g. /dashboard-operations/team-roles-permissions) with a redirect from /untitled-page.
10. Cursor-based pagination names its cursor "offset" (minor)
Location: /developer-resources/pagination
Problem: The page explains "Cursor based pagination utilizes a special token (called an offset)…" and warns "Do not modify, decode, or generate your own offset values." offset universally connotes numeric skip-based pagination — the exact thing this API doesn't do.
Consequence: Developers and agents primed by the parameter name will try offset=100-style values; the docs have to spend a warning box fighting their own naming. The two unnamed ParamField blocks on the page also render without parameter names in the export, so the limit parameter's actual name never appears.
The fix: Keep the API field but lead with "the offset parameter is an opaque cursor, not a numeric offset," and ensure the ParamField names (limit, offset) render in all export surfaces.
What they do well
Full agent-readable surface: llms.txt, a ~266 KB llms-full.txt, per-page .md exports, and a published OpenAPI spec (/developer-resources/endpoints/openapi.json).
Honest operational detail where many payment docs are vague: webhook timeout (2s) and retry backoff, idempotency key expiry (24h), subscription retry schedule (4/8/12/24h), and per-method geography/currency tables.
Consistent webhook security story with two documented verification schemes (HMAC-SHA512 and ECDSA) and sample headers.
Top 3 recommendations
Fix the getting-started usage example (Basic auth, correct /payments/create-intent path, real request body) — it's the page most humans and agents read first, and both lines are currently wrong.
Reconcile the idempotency/error-code contract (400 vs 409 for concurrent duplicates) and the rate-limit units (per minute vs per second) so retry logic can be written from any one page.
Run every code sample through a compile/lint pass — the Java webhook verifier doesn't compile and the payment-status JS example can never report success.