Dodo Payments
Configure the Dodo Payments provider — API key, the separate test host, webhook signing secret, limitations, and the traps of its unversioned API.
Dodo Payments is a merchant of record. Import the factory from revenue-sdk/dodo-payments.
import { createClient } from 'revenue-sdk';
import { dodoPayments } from 'revenue-sdk/dodo-payments';
const client = createClient({
provider: dodoPayments({ apiKey: process.env.DODO_PAYMENTS_API_KEY! }),
});
Factory options
apiKey?string
Dodo Payments API key, sent as a Bearer credential.
stringserver?'live' | 'test'
Selects live.dodopayments.com or test.dodopayments.com.
'live' | 'test'livebaseUrl?string
Overrides server; used verbatim.
stringfetch?typeof fetch
Custom fetch implementation.
typeof fetchAuthentication
Create an API key in the Dodo Payments dashboard under Developer → API keys.
Sandbox & test mode
Test and live are separate hosts with separate keys and separate catalogs — a test key does not authenticate against the live host:
dodoPayments({ apiKey: process.env.DODO_PAYMENTS_TEST_API_KEY!, server: 'test' });
Limitations
- No pause or resume. Dodo has neither an endpoint nor a paused subscription status, so
subscriptions.pauseandsubscriptions.resumethrowunsupported(pause: false) andpauseAtPeriodEndis alwaysfalse. - No end-trial operation.
subscriptions.endTrialthrowsunsupported(endTrial: false). - No
prorateproration. There is no defer-to-next-invoice mode, onlyprorated_immediatelyanddo_not_bill. OmittingprorationBehaviorbehaves asinvoice_now. - No checkout expiry override. It is fixed at 24 hours — 15 minutes when the session is created
with
confirm=true— socheckouts.create({ expiresAt })throwsunsupported. - No pay-what-you-want. Dodo’s field is per cart item and one-time products only, while
customAmountis one checkout-level amount, socheckouts.create({ customAmount })throwsunsupported. Let the buyer name the amount on Dodo’s own checkout page instead — see Pay what you want. checkouts.getreturns no URL. The status endpoint omits it, so aCheckoutread back hasurl: ''. Persist the URL fromcheckouts.create.
Full values for every capability: capability matrix.
Webhooks
Create the endpoint in the dashboard under Developer → Webhooks and copy the signing secret
(whsec_…). Verification and parsing come from the subpath:
import { parseWebhookEvent, verifyWebhook } from 'revenue-sdk/dodo-payments';
See Handle webhooks for the full handler.
Events worth subscribing to: subscription.active, subscription.updated, subscription.renewed,
subscription.plan_changed, subscription.update_payment_method, subscription.on_hold,
subscription.cancelled, subscription.expired, subscription.failed, payment.succeeded, and — if
you sell license keys — license_key.created.
subscription.pausedis deliberately not mapped and normalizes tounknown. Dodo has no pause endpoint and no paused status, so nothing upstream can produce it — handling it would imply a capability that does not exist.- Only one transition is named:
subscription.on_hold→subscriptionChange: 'past_due'. Everything else leaves itundefined, including a scheduled cancellation — readevent.subscription.cancelAtPeriodEndfor that.
License keys
The /licenses/* routes need no credential, so validate, activate, and deactivate are standalone
exports rather than client methods — safe to ship inside a desktop, mobile, or CLI app. See
License keys for the shared signatures and return types.
Dodo-specific options — the rest (key, activationId, label, fetch, signal) are the same on
every provider:
server?'live' | 'test'
Same hosts as the factory.
'live' | 'test'livebaseUrl?string
Overrides server; used verbatim.
string- The public license routes carry the tightest rate limits of the three providers that support license keys — roughly 20 requests per second and 100 per minute. Cache the verdict in your app instead of validating on every action.
- The merchant license endpoints are marked
@deprecatedupstream in favour of an entitlements-based replacement, but still function;client.licenseKeyscontinues to use them.
Orders
See Orders for the model. A unified Order is a Dodo Payments payment.
Dodo specifics:
- Only succeeded payments are listed.
processing, therequires_*states,failedandcancelledare visible only through a directorders.get. - The sort order is unverified.
/paymentstakes no sort parameter and Dodo documents no ordering guarantee, so the sequence is whatever the API returns. Sort bycreatedAtif it matters. orders.getInvoiceUrlthrowsnot_foundwhen the payment has noinvoice_url. Dodo exposes the invoice as a field rather than an endpoint (GET /invoices/payments/{id}returns PDF bytes), and the field is missing from list rows — so the call re-reads the payment.
Provider notes
- The product is the purchasable unit. Dodo prices have no identifier of their own, so
Price.id,Price.checkoutRef, andProduct.idare all the product ID, and each product has at most one price. - Unversioned, additive API. There is no version header and fields are added without notice, so the
SDK’s wire types are tolerant, enums are treated as open, and ID prefixes are never validated.
Unrecognized subscription statuses fall back to
active. subscriptions.changePlancosts an extra request. Dodo’s change-plan endpoint returns an empty body, so the SDK re-fetches the subscription to return its updated state.- A customer needs both a name and an email, which is one of the two reasons
customers.createtakesnameas a required parameter.metadatais supported and is replaced wholesale on update. - Usage events require an
event_id. WhenidempotencyKeyis omitted the SDK generates a UUID — unique per attempt, so that call is not replay-safe. Atimestampmay be at most one hour old (and 5 minutes ahead), otherwise the request is rejected with400; you cannot backfill. An unknowncustomer_idis dropped silently rather than erroring. See Usage-based billing.