Dodo Payments Documentation Audit
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)
Location: /developer-resources/sdks/cli, /developer-resources/webhooks/intents/webhook-events-guide, /developer-resources/subscription-integration-guide
Problem: The same events are spelled differently depending on which page you read.
- CLI page lists
payment.success,refund.success, andlicence.created. - Webhook Events Guide (canonical) lists
payment.succeeded,refund.succeeded, andlicense_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 (alsoplan_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)
Location: /developer-resources/webhooks/intents/payment
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)
Location: /developer-resources/webhooks/intents/subscription
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)
Location: /features/subscription → "Detecting Trial Status"
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)
Location: /changelog/v1.97.6, /features/entitlements/introduction, /developer-resources/webhooks/intents/webhook-events-guide
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 newentitlement_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)
Location: /api-reference/introduction → "Business Tiers"
Problem: The Business Tiers table renders three rows of numbers with no column headers:
| Tier 0 (Default) | 40 | 240 |
| Tier 1 | 100 | 1,000 |
| Tier 2 | 500 | 5,000 |
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)
Location: /api-reference/introduction, /api-reference/error-codes
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)
Location: /introduction (marketing surface), /developer-resources/dodo-payments-sdks, /developer-resources/sdks/typescript
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.txtexists 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/noand 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.successvspayment.succeededandlicence.createdvslicense_key.createdare 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.deliveredas 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.