Dodo Payment Service
Turn one-time Dodo Payments Checkout sessions into global balance topups.
PaymentService with dodoPaymentProvider() creates Dodo Payments Checkout sessions and completes an existing balance topup after Dodo sends a successful payment webhook.
The service only owns Checkout creation, local payment records, webhook event records, and the final call to balance.finishTopup().
Enable it
import { Federation } from "@downcity/city";
import {
BalanceService,
PaymentService,
dodoPaymentProvider,
} from "@downcity/services";
const base = new Federation({ db });
const balance = new BalanceService();
base.use(balance);
base.use(new PaymentService({
// PaymentService takes `readTopup` and `finishTopup` directly.
// There is no balance bridge object anymore.
readTopup: async (topup_id) => await balance.readTopup(topup_id),
finishTopup: async (topup_id, extra) => await balance.finishTopup(topup_id, extra),
providers: [
dodoPaymentProvider({
api_key: process.env.DODO_PAYMENTS_API_KEY,
product_id: process.env.DODO_PRODUCT_ID,
webhook_key: process.env.DODO_WEBHOOK_KEY,
}),
],
}));Required env
DODO_PAYMENTS_API_KEY: API key used by thedodopaymentsSDKDODO_PRODUCT_ID: Dodo product used for CheckoutDODO_WEBHOOK_KEY: optional webhook signing keyDODO_ENVIRONMENT: optionaltest_modeorlive_mode; defaults totest_modeDOWNCITY_CITY_BASE_URL: optional public City base URL used to derive built-in result pagesDODO_CURRENCY: optional display currency for payment method discoveryDODO_API_BASE_URL: optional API base URL override for tests
Dodo return and cancel URLs are generated automatically from DOWNCITY_CITY_BASE_URL. If it is not configured, the service falls back to the current request origin for local development.
Frontend flow
Create a trusted topup first through balance, then create a Dodo checkout for it:
const checkout = await user.service("payment").action("checkout/create").invoke({
method_id: "dodo",
topup_id: "topup_demo",
});
location.href = checkout.checkout_url;PaymentService() exposes Dodo through:
const methods = await guest.service("payment").get("methods");The Dodo method is enabled only when both DODO_PAYMENTS_API_KEY and DODO_PRODUCT_ID are configured.
Webhook sync
Configure Dodo to send webhook events to:
POST /v1/payment/webhookThe service applies payment.succeeded once per event id. Repeated events return the stored sync status and do not credit balance twice.
Routes
GET /v1/payment/methodsPOST /v1/payment/checkout/createGET /v1/payment/payments/meGET /v1/payment/paymentsGET /v1/payment/eventsPOST /v1/payment/webhookGET /v1/payment/redirect/successGET /v1/payment/redirect/cancel