DEV Community

jidong
jidong

Posted on • Originally published at jidonglab.com

Building the Toss Payments Pipeline — and How Korean Checkout Was Silently Broken for 12 Hours

The session started at 00:35 UTC. The goal was straightforward: wire up Toss Payments end-to-end. Checkout widget, server-side verification, webhook, receipt email — all in one shot.

Twelve hours later, Korean checkout was completely dead.


The Toss Pipeline: Three Hours at Midnight

The entire payment pipeline from widget to confirmation email needed to be built from scratch. Each step designed to fail independently without corrupting order state.

The /checkout/toss page is a thin wrapper around @tosspayments/tosspayments-sdk. On completion, it redirects to a success page which fires a server-side verification request. The webhook handles Toss's async confirmation event with HMAC-SHA-256 signature verification — if this gets missed, orders stay pending forever.

POST /api/checkout/toss/confirm  (browser → server, synchronous)
POST /api/checkout/toss/webhook  (Toss → server, async, HMAC-verified)
Enter fullscreen mode Exit fullscreen mode

The receipt email (sendReceiptEmail.ts) packs all 8 locales into one file. 310 lines, but the actual rendering logic is shared — only the string tables differ.


2c58ca3 feat: add Toss Payments integration and business info footer eb3b125 feat: add Toss webhook, payment receipt email (8-locale i18n) 5f7ef27 feat: add coming-soon confirmation email, fix checkout test 6646720 디자인 시스템 통일 및 이메일 업데이트 3new email types 8locales supported 523lines added (webhook + receipt)

Design System Cleanup

01:27 UTC. More CSS than code.

globals.css had four fonts: Pretendard, Sora, Manrope, Italiana. They accumulated from different sessions, each engineer (or AI) dropping in whatever felt right at the time. Reduced to two: Pretendard for body, Outfit for display headings.

Button border-radius was scattered — 8px here, 12px there, 16px somewhere else. Unified to 14px. Card glassmorphism effects had no consistent shadow scale. Each component felt subtly different from the next, and that inconsistency erodes user trust without them knowing why.

Item Before After
Font count 4 (Pretendard, Sora, Manrope, Italiana) 2 (Pretendard, Outfit)
Button radius 8px / 12px / 16px mixed 14px unified
Contact email different across 8 locales dbswn428@gmail.com unified

The Critical Bug: Korean Checkout Silent-Failed for 12 Hours

12:20 UTC. Any Korean user who tried to pay during those 12 hours got a 500 error.

The cause was a single line in packages/shared/src/config/countries.ts:

paymentProvider: "paddle",  // wrong. Korea must use toss.
Enter fullscreen mode Exit fullscreen mode

During the midnight Toss integration work, the Korean market's paymentProvider had been silently flipped from "toss" to "paddle". Paddle has no production API key. So every Korean checkout attempt hit /api/checkout/paddle/create, which threw PADDLE_API_KEY is not set, which returned a generic 500.

The debugging path is worth noting. The first assumption was a DB connection issue — because the error message was useless.

// before commit 9d5e4e9
message: 'Checkout creation failed.'

// after
const errMsg = err instanceof Error ? err.message : String(err);
message: `Checkout creation failed: ${errMsg}`
Enter fullscreen mode Exit fullscreen mode

Exposing the actual error string immediately surfaced PADDLE_API_KEY is not set. From there: add a Paddle fallback to Toss when the key is missing, then trace back to find the root cause in countries.ts.


9d5e4e9 debug: expose checkout error message for diagnosing DB connection issue 099a202 fix: fallback to Toss when Paddle API key not configured 8704207 fix: restore Korean paymentProvider to toss (critical)

Debugging time: 34 minutes. 3 commits. Root cause: 1 line.


What This Session Showed

A file named countries.ts looks like configuration. It doesn't feel critical. But it's actually the branching point for the entire payment routing logic — one wrong string and every Korean user hits a wall. This file needs a test.

Generic error messages double debugging time. Checkout creation failed. tells you nothing about whether the problem is a DB connection, a missing API key, or a network timeout. Even if you don't expose details to users, server-side error messages need to be specific.

  • Toss checkout widgetKorean-market `/checkout/toss` page with SDK widget integration
  • Toss webhookHMAC-SHA-256 signature verification, async order confirmation
  • Receipt emailAuto-sent on payment completion, 8-locale i18n
  • Coming-soon emailFeature launch notification with early-bird discount promise
  • Paddle fallbackAuto-routes to Toss when Paddle API key is absent — safety net during global rollout

📌 Originally published at Jidong Lab
More AI news and dev logs → jidonglab.com

Top comments (0)