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:push

bunx 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-secret

Use 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 --prod

Set 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

  1. Call api.tosspayments.createCheckoutSession from an authenticated UI.
  2. Pass the returned order data to TossPaymentWindowButton.
  3. The payment window returns to /api/tosspayments/success or /api/tosspayments/fail.
  4. The success route validates the checkout token and confirms the server-owned amount with Toss Payments.
  5. Use getPayment, queryPayment, or cancelPayment for 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_prepaid
  • monthly_anchor_prepaid_prorated
  • monthly_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 onTossPaymentStatusChanged and onTossSubscriptionStatusChanged hooks.

Test with Toss Payments test keys first, including duplicate callbacks, amount mismatches, expired tokens, cancellation, failed renewals, and retry exhaustion, before enabling live keys.