The docs cover a broad payment platform with SDKs, MCP server, and an llms.txt index, but several pages contradict each other on webhook event names and payload coverage — landmines for both human integrators and AI coding agents, especially around fulfillment and license provisioning.
1. Webhook event names contradict across three canonical pages (critical)
Problem: The same events are spelled differently depending on which page you read.
CLI page lists payment.success, refund.success, and licence.created.
Webhook Events Guide (canonical) lists payment.succeeded, refund.succeeded, and license_key.created.
The Subscription Integration Guide enumerates only 5 subscription events (active, updated, on_hold, failed, renewed); the Subscription intent page and CLI page list 8 (also plan_changed, cancelled, expired).
The CLI surface also uses British spelling more broadly — e.g., the command dodo licences list — while the API and webhook surfaces use American (license_key.created, /api-reference/licenses/...). This is a CLI-wide divergence, not a single typo.
Consequence: A developer (or an AI agent generating webhook handlers) who copies the CLI page will write a switch case for payment.success that never fires in production, because the actual event type emitted is payment.succeeded. Similarly licence.created vs license_key.created — a one-character difference that silently breaks license provisioning. Subscription handlers built from the Integration Guide will silently miss cancellations and expirations.
The fix: Pick one canonical list (the Webhook Events Guide is the most complete) and rewrite the CLI and Subscription Integration pages to import from / link to it. Add a CI check that webhook event strings in docs match the enum in the OpenAPI spec. Standardize on American spelling across CLI command names too.
2. Payment webhook intent page is empty (critical)
Problem: The Payment intent page contains only a title and a one-line description — "The payload sent to your webhook endpoint when a payment is created or updated." No payload schema, no field reference, no example payload.
Consequence: A developer trying to type their payment.succeeded webhook handler has nowhere on this page that tells them what fields are in the payload. They have to either trigger a real webhook and inspect it, or guess from the OpenAPI spec. Agents indexing this page get the heading and one sentence — they will confidently hallucinate field names.
The fix: Populate the page with the full payload schema (TypeScript-style or JSON Schema), a complete example payload per payment event type, and a field-by-field table.
3. Subscription webhook intent page has events but no payload schema (significant)
Problem: The Subscription intent page includes the title, description, and a full 8-row event table, then a brief "Using subscription.updated for Real-Time Sync" section — but no payload schema, no example body, and no per-event field reference. A reader knows which events fire but not what JSON shape arrives.
Consequence: Developers wiring subscription.plan_changed or subscription.cancelled handlers cannot type their handlers from the docs. They have to capture a real webhook in test mode to see the shape, which is hostile to agents trying to scaffold a webhook handler from docs alone.
The fix: Add a complete signed example payload for each of the 8 events listed, plus a field-by-field schema table. Cross-link to the canonical webhook signature verification guide.
4. Deprecated "Create One Time Payment" endpoint still has a documented page with no migration callout (significant)
Location:/api-reference/payments/post-payments
Problem: The Create One Time Payment page is explicitly marked: "Deprecated API: This API will be deprecated soon. We recommend using Checkout Sessions instead." The recommendation is one sentence at the top — there is no side-by-side migration example, no removal date, and no banner showing how the equivalent Checkout Session request looks. The endpoint also remains in the docs index (llms.txt), so AI agents indexing the docs surface will treat it as a live, supported endpoint.
Consequence: AI coding agents pulling examples from the docs index have no signal beyond a single sentence that this endpoint is deprecated; many will pick it because it's the most direct "create a payment" verb. Developers who do read the warning are left without a concrete migration recipe and have to translate request fields themselves.
The fix: Add a "Migrate to Checkout Sessions" section at the top of the deprecated page with the exact equivalent request as a side-by-side. Mark the entry as deprecated in llms.txt (or remove it). Spot-check integration guides and replace any direct links to /api-reference/payments/post-payments with /api-reference/checkout-sessions/create.
5. Trial detection requires a documented, inefficient workaround (significant)
Problem: The docs themselves admit: "Currently, there is no direct field to detect trial status. The following is a workaround that requires querying payments, which is inefficient. We are working on a more efficient solution." The recommended detection method is "list payments for the subscription, and if there is exactly one payment with amount 0, the subscription is in trial."
Adjacent on the same page: trial_period_days is documented as accepting any value between 0 and 10,000 days. 10,000 days is roughly 27 years — either an undocumented product decision (effectively-infinite trial) or a validation typo, and there's no explanation of why the upper bound is what it is.
Consequence: The trial detection heuristic is brittle — anything that creates a $0 payment for any other reason (promo, comp, test) breaks the inference. Every dashboard that shows "you're on a trial" has to make N+1 API calls per subscription render. The 10,000-day cap, meanwhile, lets a misconfigured request create a 27-year trial; with no documented reason for the bound, integrators can't tell whether to add their own validation.
The fix: Ship a trial_status or is_in_trial boolean (or current_period_type: trial|active) on the Subscription resource. Document the rationale and intended cap for trial_period_days, or lower it to a sane maximum (e.g., 365). Until then, document a webhook-based detection path so developers don't have to poll.
6. New entitlement webhook events not back-propagated; Entitlements introduction still teaches the deprecated pattern (significant)
Problem: Changelog v1.97.6 (May 7, 2026) introduces four new webhook events: entitlement_grant.created, .delivered, .failed, .revoked, and explicitly tells new integrators "listen to entitlement_grant.delivered rather than payment.succeeded. Payment success doesn't mean delivery is finished, especially for OAuth-based integrations."
But:
The canonical Webhook Events Guide page (which lists Payment / Refund / License Key / Credit events) does not include the entitlement events at all.
The Entitlements introduction page is itself the place where the deprecated pattern is most prominently taught — its "Grant Behavior by Event" table is keyed off payment.succeeded, subscription.active, subscription.renewed, etc., not the new entitlement_grant.* events. A developer reading the feature page is given the exact fulfillment model the changelog tells new integrators to abandon.
Consequence: A developer reading either the Webhook Events Guide or the Entitlements introduction as the source of truth will continue using payment.succeeded for fulfillment — exactly what the changelog says not to do. This is particularly bad for OAuth-based integrations where payment success ≠ delivery success.
The fix: Add an "Entitlement Events" section to the Webhook Events Guide. Rewrite the Entitlements introduction so the "Grant Behavior" table is reframed as "what triggers grants internally," with a separate "What you should listen to" section pointing at entitlement_grant.delivered. Update any "fulfillment after payment" examples accordingly.
7. Rate limit table for Business Tiers has no header row and no documented upgrade path (significant)
There is no header row identifying which column is per-second vs per-minute, no explanation of how to upgrade tier or what triggers the upgrade, no example 429 response body, and the rate-limit response headers (X-RateLimit-Limit, -Remaining, -Reset) are listed as bullets rather than a structured spec.
Consequence: A developer planning capacity has to guess the columns by inferring from the Default Limits table above. There's no documented path to request Tier 1 or Tier 2 — is it self-service? Sales? Automatic at volume? Without an example 429 body, agents generating retry/backoff logic have to invent the failure-shape they're parsing.
The fix: Add header row (Tier | Per Second | Per Minute). Add a paragraph explaining how tiers are assigned and how to request an upgrade. Document the 429 response body with a real example, and structure the rate-limit headers as a typed table linked from the 429 row in the error codes table.
8. Error response format documented but error codes not enumerated, and the two pages disagree on shape (significant)
Problem: The Introduction page shows an Error Response example as {"code": "INVALID_REQUEST", "message": "..."}. The Error Codes page documents only HTTP status codes (400, 401, 403, …) in a table and then has an "Error Response Format" heading with no body content visible — so the structured-error shape from the Introduction page is never re-stated alongside the HTTP table. The set of possible code string values (INVALID_REQUEST, etc.) is never enumerated. Compare with the Transaction Failures page, which does enumerate AUTHENTICATION_FAILURE, CARD_DECLINED, etc., but only for transaction-level errors, not API errors.
Consequence: A developer writing if (err.code === '???') has no list to choose from — they will either match on message (which can change) or branch only on HTTP status (losing precision). And because the two pages don't agree on which shape is canonical, integrators cannot tell whether to expect a code string at all.
The fix: Publish the full enum of code values used in API error responses, in the same structured table format as Transaction Failures, and link both pages to each other. Repeat the structured-error shape on the Error Codes page so it matches the Introduction. Mirror the enum in the OpenAPI spec so SDKs can generate typed error classes.
9. Marketing page advertises 8 SDKs; only TypeScript clearly has a dedicated reference page (significant)
Problem: The Introduction page prominently lists 8 SDKs as icon cards: TypeScript, Python, Go, PHP, Java, Kotlin, C#, Ruby. Each card links to an anchor on a single SDKs page (e.g. #python, #go), not to a dedicated per-SDK reference. The TypeScript SDK has its own dedicated page (/developer-resources/sdks/typescript); the visible docs surface and the llms.txt index do not show equivalent dedicated pages for the other languages — though the scrape is partial, so this should be verified rather than asserted as absence.
Consequence: A Java or Kotlin developer who clicks the marketing card lands on an anchor in a long single page rather than a full SDK reference equivalent to the TypeScript page. The depth of support across languages is unclear, and an agent picking an SDK by quality of docs will pick TypeScript regardless of the user's actual stack.
The fix: Audit whether each advertised SDK has a dedicated reference page; if not, give every SDK its own page at /developer-resources/sdks/<lang> with the same structure as the TypeScript page (install, auth, checkout session example, webhook verification). Where parity isn't there yet, mark non-parity SDKs as "Beta" or "Community" on the Introduction page so the marketing claim matches reality.
10. TypeScript checkout example uses placeholders that aren't marked as such (minor)
Location:/developer-resources/checkout-session
Problem: The TypeScript example uses values like prod_123, customer@example.com, John Doe, +1234567890, https://yoursite.com/checkout/success, order_123. Only prod_123 carries an inline comment (// Replace with your actual product ID); the others read like real values an agent might inline.
Consequence: An AI agent extracting this snippet to build an integration may keep +1234567890 or https://yoursite.com/... as-is, because nothing programmatically marks them as placeholders. The phone number in particular passes basic validation and could end up in a created session.
The fix: Use a consistent placeholder convention across all examples — either <your_product_id>-style angle brackets or YOUR_PRODUCT_ID in screaming snake — and add a single "Placeholders to replace" callout above the snippet listing all of them.
11. Webhooks page advertises an "Example Payload" section with no payload visible (minor)
Location:/developer-resources/webhooks
Problem: The webhooks page has the retry table and 15-second timeout well-specified, then an ### Example Payload heading. The scrape ends at this heading with no payload shown — combined with the empty Payment intent page (finding #2), there is no visible end-to-end "here's exactly what we POST to your endpoint" example anywhere on the surface.
Consequence: Developers cannot verify their parser against a known-good payload from the docs alone. They have to trigger a real webhook in test mode to see the shape — fine for humans, hostile to agents trying to scaffold a webhook handler.
The fix: Inline a complete signed example payload (with event, data, created_at, signature header) on the main webhooks page, and link it from the (now-populated) intent pages.
What they do well
llms.txt exists and indexes the full docs surface — the right move for AI agent discoverability, even if the pages it points to are uneven.
The Transaction Failure Reasons page is a model of the right structure — every code, with User Error: yes/no and a description. The rest of the error and webhook docs should follow this pattern.
The MCP server uses Code Mode (TypeScript SDK in a sandbox) instead of exposing hundreds of granular tools — a more durable design for agent-driven payment flows than tool-per-endpoint.
Top 3 recommendations
Unify webhook event names across CLI, Subscription Integration Guide, and Webhook Events Guide today — payment.success vs payment.succeeded and licence.created vs license_key.created are silent bugs, and the British/American split runs through the whole CLI surface.
Populate the Payment intent page and add payload schemas to the Subscription intent page — these are the most consequential blank/partial pages in the docs, directly behind every webhook handler an integrator writes.
Rewrite the Entitlements introduction to teach entitlement_grant.delivered as the primary fulfillment hook, and add the entitlement events to the canonical Webhook Events Guide — right now the feature docs still teach the pattern the changelog explicitly tells new integrators to abandon.
Verified 70 documentation code blocks across the 21 source pages.
PASS (15): package installation commands (npm install dodopayments, npm install -g dodopayments-cli), the npx skills add dodopayments/skills bootstrap, all JSON response/config samples (intro response shapes, checkout-session response shape, every MCP server JSON config including the combined Knowledge + API config).
FAIL (6): every executable SDK/REST snippet that calls client.checkoutSessions.create or client.payments.create. Two distinct documentation problems surface:
The Node.js SDK and REST snippets on /developer-resources/checkout-session hard-code phone_number: '+1234567890'. The API rejects it with 400 customer.phone_number: is invalid (Invalid Phone number), so the example fails before the placeholder product is even evaluated. Suggested fix in docs: replace with an E.164 number that passes validation (e.g. +14155552671) or drop the field from the example.
The deprecated Create One Time Payment x-codeSamples on /api-reference/payments/post-payments instantiate new DodoPayments({ bearerToken: ... }) with no environment. The SDK defaults to live_mode, so the documented test-mode flow returns 401 Unauthorized when used with a test key. Suggested fix: add environment: 'test_mode' to the example (matching every other quick-start on the docs site).
Quick Start (/developer-resources/sdks/typescript) and the subscription integration Node/REST snippets (/developer-resources/subscription-integration-guide) execute fine but the API rejects the literal placeholder product IDs (product_id, prod_subscription_monthly) with 422 Product ... does not exist. Suggested fix: either prefix placeholders so they cannot be confused with real IDs (<your_product_id>) or use a documented "fake" product the docs tell readers to create first.
SKIP (49): partial fragments that omit imports/client initialization (most of /api-reference/metadata, the multi-step common-operations blocks on /developer-resources/sdks/typescript, and per-feature TS snippets on /features/subscription and /changelog/v1.97.6); webhook handler examples that need a long-running HTTP server with an inbound webhook; runtime-unavailable samples (Python — pip not installed in this sandbox; Go/Java/Kotlin/Ruby; Bun/Deno/yarn/pnpm); interactive CLI commands (dodo login, dodo wh listen, dodo wh trigger); and pure config/URL templates (customer portal static link, env exports).
Evidence-level docs concerns flagged in the audit research (CLI events page using payment.success/licence.created vs. canonical payment.succeeded/license_key.created; Subscription Integration Guide listing 5 events vs. the canonical 8; metadata page referencing event.data.object.metadata vs. canonical event.data.metadata) are content/consistency bugs that this verifier surfaced but cannot reproduce by execution; they remain as written in the live docs.
Required credentials
DODO_PAYMENTS_API_KEY — test-mode bearer token, used for every executable SDK/REST snippet.
401 Unauthorized. The example omits environment, so the SDK defaults to live_mode; a test-mode key cannot reach live.dodopayments.com. Suggested: add environment: 'test_mode' or note the requirement.
422 Product product_id does not exist. The literal placeholder 'product_id' is sent as-is; the snippet has no comment marking it as a placeholder. Suggested: prefix placeholders (e.g. <your_product_id>) or add a // replace this comment matching other pages.
5
bash .env example
SKIP
Env declaration only, not a runnable command.
6
typescript timeout config
SKIP
Partial fragment — re-declares client without import.
400 customer.phone_number: is invalid (Invalid Phone number). The hard-coded +1234567890 fails E.164 validation before the placeholder product is evaluated. Suggested: use a phone that passes validation (e.g. +14155552671) or omit the field.
2
python SDK checkout_sessions.create
SKIP
Runtime unavailable.
3
javascript REST fetch /checkouts
FAIL
Same 400 Invalid Phone number failure as #1, identical hard-coded +1234567890.