Skip to content
revenue-sdk
Esc
navigateopen⌘Jpreview
On this page

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

PropType
apiKey?string

Dodo Payments API key, sent as a Bearer credential.

Typestring
server?'live' | 'test'

Selects live.dodopayments.com or test.dodopayments.com.

Type'live' | 'test'
Defaultlive
baseUrl?string

Overrides server; used verbatim.

Typestring
fetch?typeof fetch

Custom fetch implementation.

Typetypeof fetch

Authentication

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.pause and subscriptions.resume throw unsupported (pause: false) and pauseAtPeriodEnd is always false.
  • No end-trial operation. subscriptions.endTrial throws unsupported (endTrial: false).
  • No prorate proration. There is no defer-to-next-invoice mode, only prorated_immediately and do_not_bill. Omitting prorationBehavior behaves as invoice_now.
  • No checkout expiry override. It is fixed at 24 hours — 15 minutes when the session is created with confirm=true — so checkouts.create({ expiresAt }) throws unsupported.
  • No pay-what-you-want. Dodo’s field is per cart item and one-time products only, while customAmount is one checkout-level amount, so checkouts.create({ customAmount }) throws unsupported. Let the buyer name the amount on Dodo’s own checkout page instead — see Pay what you want.
  • checkouts.get returns no URL. The status endpoint omits it, so a Checkout read back has url: ''. Persist the URL from checkouts.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.paused is deliberately not mapped and normalizes to unknown. 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_holdsubscriptionChange: 'past_due'. Everything else leaves it undefined, including a scheduled cancellation — read event.subscription.cancelAtPeriodEnd for 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:

PropType
server?'live' | 'test'

Same hosts as the factory.

Type'live' | 'test'
Defaultlive
baseUrl?string

Overrides server; used verbatim.

Typestring
  • 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 @deprecated upstream in favour of an entitlements-based replacement, but still function; client.licenseKeys continues 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, the requires_* states, failed and cancelled are visible only through a direct orders.get.
  • The sort order is unverified. /payments takes no sort parameter and Dodo documents no ordering guarantee, so the sequence is whatever the API returns. Sort by createdAt if it matters.
  • orders.getInvoiceUrl throws not_found when the payment has no invoice_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, and Product.id are 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.changePlan costs 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.create takes name as a required parameter. metadata is supported and is replaced wholesale on update.
  • Usage events require an event_id. When idempotencyKey is omitted the SDK generates a UUID — unique per attempt, so that call is not replay-safe. A timestamp may be at most one hour old (and 5 minutes ahead), otherwise the request is rejected with 400; you cannot backfill. An unknown customer_id is dropped silently rather than erroring. See Usage-based billing.

Last updated on August 8, 2026

Was this page helpful?