Stripe Payment Service

Topups and Checkout

Why one-time Stripe recharge should be built on top of balance topups instead of updating wallet balance directly.

In this version, @downcity/services is not a subscription or entitlement system. It is about this:

let a one-time Stripe payment complete a topup that already exists in City.

Why topup must exist first

"The user wants to recharge" is not the same as "the wallet has been credited."

Inside Downcity, the more stable model is:

  1. create a pending topup
  2. send the user to Stripe
  3. complete that topup after payment succeeds

This gives you:

  • a trusted amount source inside City, not inside the frontend
  • a stable mapping between Stripe payment records and internal topups
  • one wallet-credit path through balance.finishTopup()

Common flow

const topup = await user.service("balance").action("topups/create").invoke({
  amount: 500,
  note: "recharge",
});

const checkout = await user.service("payment").action("checkout/create").invoke({
  method_id: "stripe",
  topup_id: topup.topup_id,
});

The frontend then redirects to checkout.checkout_url and Stripe handles the hosted payment page.

Why not let the Stripe service own the raw amount

If the frontend passes the raw amount directly into the Stripe service, several problems appear:

  • the amount is easier to tamper with
  • you lose a unified internal topup record
  • success, failure, and expiration become harder to reconcile

The key is not only "how to take payment." The key is "City creates the trusted topup first, and Stripe completes it."

Common API surface

  • POST /v1/balance/topups/create
  • POST /v1/payment/checkout/create