Stripe Payment Service
Use @downcity/services to turn one-time Stripe payments into global balance topups.
The Stripe payment service currently does one thing:
after a user completes a one-time Stripe payment, sync that result into a
balance topup.
It does not handle subscriptions or studio access grants.
Enable
import { balanceService } from "@downcity/services";
import {
paymentService,
stripePaymentMethod,
stripePaymentService,
type StripePaymentServiceBalanceBridge,
} from "@downcity/services";
const balance = balanceService({
unit: "credits",
});
const stripeBalanceBridge: StripePaymentServiceBalanceBridge = {
readTopup: async (topup_id) => await balance.readTopup(topup_id),
finishTopup: async (topup_id, extra) => await balance.finishTopup(topup_id, extra),
};
base.use(balance);
base.use(paymentService({
methods: [
stripePaymentMethod(),
],
}));
base.use(stripePaymentService({
balance: stripeBalanceBridge,
secret_key: process.env.STRIPE_SECRET_KEY,
webhook_secret: process.env.STRIPE_WEBHOOK_SECRET,
}));Common flow
1. Read payment methods first
const methods = await guest.service("payment").get("methods");The frontend can read methods.items first, then decide whether to show a Stripe recharge entry on the current City.
2. Create a topup first
const topup = await user.service("balance").action("topups/create").invoke({
amount: 500,
note: "recharge",
});3. Create a Stripe Checkout Session
const checkout = await user.service("payment.stripe").action("checkout/create").invoke({
topup_id: topup.topup_id,
});The response includes:
payment_idstripe_checkout_session_idcheckout_url
The frontend can redirect to checkout_url.
4. Stripe calls the webhook
Stripe should call:
POST /v1/payment.stripe/webhookSo when you register the endpoint in Stripe Dashboard, the default value is:
{base_url}/v1/payment.stripe/webhookWhen webhook_secret or STRIPE_WEBHOOK_SECRET is configured, the service verifies stripe-signature.
When it receives checkout.session.completed, the service calls balance.finishTopup() and credits the wallet.
If you deploy on Workers, write both STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET into City env after deployment.
If STRIPE_SUCCESS_URL / STRIPE_CANCEL_URL are not configured explicitly, the service derives default redirect pages in this order:
DOWNCITY_CITY_BASE_URL- the current request origin
The derived paths are:
/v1/payment.stripe/redirect/success/v1/payment.stripe/redirect/cancel
That means many setups no longer need STRIPE_SUCCESS_URL / STRIPE_CANCEL_URL, and often do not need to configure DOWNCITY_CITY_BASE_URL either.
Only override them when you want Stripe to return users to your own frontend pages:
STRIPE_SUCCESS_URLSTRIPE_CANCEL_URL
Relationship with balance
The responsibility split is important:
balanceowns the global wallet, topups, and ledgerpayment.stripeowns Stripe Checkout, webhook handling, and payment-result syncing
That means the Stripe service never updates balance directly. It always credits through finishTopup().
Routes
GET /v1/payment/methodsPOST /v1/payment.stripe/checkout/createGET /v1/payment.stripe/payments/meGET /v1/payment.stripe/paymentsGET /v1/payment.stripe/eventsPOST /v1/payment.stripe/webhook