Full Toss Payments integration — then one line broke all Korean checkouts
Built the entire Korean payment flow in one session, shipped receipt emails for both Toss and Paddle, cleaned up a messy design system — then spotted that a single config value had been wrong the whole time, silently routing all Korean users to an unconfigured Paddle endpoint.
The fix was one line. The miss was embarrassing.
Toss Payments: full integration
FateSaju is a Korean fortune-telling (사주, Four Pillars) app with a dual payment structure: Toss Payments for Korea, Paddle for the rest of the world. Toss is the dominant payment method in Korea — think Stripe but deeply embedded in the Korean banking and mobile ecosystem.
The previous QA session had temporarily routed Korean users to Paddle because Toss wasn’t implemented yet. This session was about doing it properly.
The prompt given to Claude covered the entire surface area at once:
“Add Toss payment widget page
/checkout/toss, success/fail redirect pages,/api/checkout/toss/confirmfor server-side verification, updateusePaywallto route Korean users to Toss, add business registration info to the footer across all 8 locales.”
It came back in one pass: SDK install, widget rendering, HMAC server verification, success/fail flow. Many files changed, but each individual delta was small and easy to review.
- Payment widget`/checkout/toss` — receives amount + orderName params, renders Toss widget
- Server verification`/api/checkout/toss/confirm` — double-checks paymentKey + amount server-side
- Success/fail pageshandles Toss redirect URLs, locale-specific error display on failure
- Business footer8 locales — company name, representative, business registration number, address
Toss webhook + receipt email
Payment confirmation needed two paths: the synchronous confirm API call the user triggers directly, and the async webhook Toss pushes server-to-server. Both need to fire the receipt email.
The webhook uses HMAC-SHA-256 signature verification with a TOSS_WEBHOOK_SECRET env var. Receipt email was wired into Toss confirm, Toss webhook, and Paddle webhook — all three paths covered.
sendReceiptEmail.ts is 326 lines. Eight locales, payment amount/order name/date table, branded HTML template, customer email, multilingual CTA. Claude generated it in one shot with no missing i18n keys.
sendComingSoonEmail.ts went in the same session — subscription confirmation for the early access list. Eight locales, early-bird discount promise, launch notification confirmation.
Design system cleanup
The codebase had five font families in active use: Pretendard, Outfit, Sora, Manrope, Italiana — mixed inconsistently across globals.css. Consolidated to two: Pretendard for Korean body text, Outfit for display/headings. Removed the rest.
Along the way: unified button border-radius to 14px, defined a proper typography scale (h1–h3, body, button, input), cleaned up glassmorphism card styles.
| Item | Before | After |
|---|---|---|
| Font families | 5 (Pretendard, Outfit, Sora, Manrope, Italiana) | 2 (Pretendard, Outfit) |
| Button radius | mixed | 14px across the board |
| Typography scale | ad-hoc values | h1–h3, body, button, input defined |
| Contact email | inconsistent across 8 locale files | [email protected] unified |
The one-line bug that killed Korean checkout
After shipping all of the above, English-locale browsers (/en/) were hitting 500 errors on checkout. Root cause: /en/ routes to Paddle, but PADDLE_API_KEY wasn’t set in the environment. Fixed with a fallback — if Paddle isn’t configured, the endpoint delegates to the Toss create flow internally.
While debugging that, a worse problem surfaced.
In packages/shared/src/config/countries.ts, there’s a per-country config object. Korea’s entry:
// packages/shared/src/config/countries.ts
paymentProvider: "paddle", // ← this was the problem
The previous QA session (2026-03-10) had a commit that deliberately set this to "paddle" — at the time, Toss wasn’t implemented and Korean routing was going to Toss, which was broken. Correct fix for that moment. But this session implemented Toss end-to-end, and the countries.ts restoration got missed. So every Korean user was being routed to Paddle, which had no API key, and getting a 500.
The fix:
paymentProvider: "toss", // ← restored
One character changed. Committed with (critical) in the message because it genuinely was.
Commit log
Three debug commits at the end of a session is a pattern worth watching. Larger features touching more files create more surface area for “restoration” bugs — where a temporary config change from a previous session gets left behind after the full implementation lands. Next time a big feature branch merges, explicitly diff any config files that were touched across both sessions.
Comments 0