Toss Payments
Add authenticated one-time payments and recurring billing with the Gencow Toss Payments starter
The Toss Payments starter adds server-owned order records, payment confirmation routes, webhook reconciliation, billing-key storage, subscription schedules, and React payment-window components.
Install
bunx gencow@latest add TossPayments
bunx gencow@latest db:generate
bunx gencow@latest db:pushbunx is the recommended runner. Node.js users can run the same commands with
npx gencow@latest.
The component adds backend files under gencow/, frontend helpers under
src/tosspayments/, and registers the procedures, HTTP routes, schema, and due
subscription cron in your project.
Environment
Keep local backend secrets in gencow/.env:
TOSS_PAYMENTS_CLIENT_KEY=
TOSS_PAYMENTS_SECRET_KEY=
TOSS_PAYMENTS_PUBLIC_BASE_URL=http://localhost:5173
TOSS_PAYMENTS_WEBHOOK_SECRET=
TOSS_BILLING_CLIENT_KEY=
TOSS_BILLING_SECRET_KEY=
TOSS_BILLING_ENCRYPTION_KEY=replace-with-a-strong-random-secretUse the separate API integration keys issued by the Toss Payments developer console. Never expose secret or encryption keys in frontend variables. Only the client key belongs in browser code.
For cloud development or production, set the same backend values with
gencow env set or push the backend env file:
bunx gencow@latest env push
bunx gencow@latest env push --prodSet TOSS_PAYMENTS_PUBLIC_BASE_URL to the public frontend URL for the target
environment so Toss can return to the generated success and failure routes.
One-Time Payment Flow
- Call
api.tosspayments.createCheckoutSessionfrom an authenticated UI. - Pass the returned order data to
TossPaymentWindowButton. - The payment window returns to
/api/tosspayments/successor/api/tosspayments/fail. - The success route validates the checkout token and confirms the server-owned amount with Toss Payments.
- Use
getPayment,queryPayment, orcancelPaymentfor owned orders.
import { useMutation } from "@gencow/react";
import { useState } from "react";
import { api } from "./gencow/api";
import { TossPaymentWindowButton } from "./tosspayments/PaymentWindow";
type CheckoutSession = {
orderId: string;
customerKey: string;
orderName: string;
amount: number;
successUrl: string;
failUrl: string;
};
function Checkout() {
const [session, setSession] = useState<CheckoutSession | null>(null);
const { mutate: createSession } = useMutation(
api.tosspayments.createCheckoutSession,
);
if (!session) {
return (
<button
onClick={async () => {
const next = await createSession({ orderName: "Pro plan", amount: 9900 });
setSession(next);
}}
>
결제 준비
</button>
);
}
return (
<TossPaymentWindowButton
clientKey={import.meta.env.VITE_TOSS_PAYMENTS_CLIENT_KEY}
{...session}
paymentMethod="CARD"
/>
);
}Treat product name and price in the example as UI input only. In a production app, derive the payable amount from a server-owned product or plan record before creating the checkout session.
Recurring Billing
The starter supports one-time billing and three recurring schedules:
anniversary_prepaidmonthly_anchor_prepaid_proratedmonthly_anchor_arrears_prorated
Start with api.tosspayments.startBillingAuth, then open
TossBillingAuthButton with the returned customer and redirect data. The billing
success route exchanges the authorization code and stores only an encrypted
billing key. Use previewSubscriptionInvoice before
createSubscription, and use cancelSubscription or retrySubscriptionNow for
user-controlled lifecycle actions.
tosspayments.runDueSubscriptionCharges is an internal procedure invoked by the
generated cron definition. Do not expose it as an anonymous mutation.
Security Contract
The generated starter provides secure defaults, but your product and entitlement logic remains application code:
- Require authentication for every user payment procedure.
- Resolve prices on the server and compare the provider amount with the stored order before marking it paid.
- Keep checkout and billing authorization tokens short-lived and single-purpose.
- Use idempotency keys for confirmation, cancellation, webhook events, and subscription charge attempts.
- Re-query Toss Payments before trusting webhook payment state; do not rely on a browser redirect or webhook body alone.
- Deduplicate provider events and subscription periods in the database.
- Encrypt billing keys at rest and never return them to the browser or logs.
- Do not grant virtual-account entitlements until the provider reports the deposit as completed.
- Make fulfillment idempotent in
onTossPaymentStatusChangedandonTossSubscriptionStatusChangedhooks.
Test with Toss Payments test keys first, including duplicate callbacks, amount mismatches, expired tokens, cancellation, failed renewals, and retry exhaustion, before enabling live keys.