Dodo Payment Service

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 the dodopayments SDK
  • DODO_PRODUCT_ID: Dodo product used for Checkout
  • DODO_WEBHOOK_KEY: optional webhook signing key
  • DODO_ENVIRONMENT: optional test_mode or live_mode; defaults to test_mode
  • DOWNCITY_CITY_BASE_URL: optional public City base URL used to derive built-in result pages
  • DODO_CURRENCY: optional display currency for payment method discovery
  • DODO_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/webhook

The 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/methods
  • POST /v1/payment/checkout/create
  • GET /v1/payment/payments/me
  • GET /v1/payment/payments
  • GET /v1/payment/events
  • POST /v1/payment/webhook
  • GET /v1/payment/redirect/success
  • GET /v1/payment/redirect/cancel