이 페이지는 AI 에이전트를 주 독자로 삼은 페이지예요. AI 도구에 컨텍스트로 첨부하거나 토스페이먼츠 MCP 서버와 함께 사용하세요. 본문은 토큰 효율을 위해 영어로 작성되어 있어요.
This page is optimized for AI agents: use it for routing and guardrails, then jump to the linked guide/reference for implementation details.
Page scope: flow model + decision rules + entry points + pitfalls + routing. Full code and details live in guide/reference pages.
Companion resource: When this page is insufficient, consult llms.txt first — the complete Toss Payments docs index in LLM-standard format. Use it to locate the precise guide/reference page for any topic not fully covered here.
Markdown source for any docs page: Append .md to any docs URL to fetch the raw markdown (more token-efficient than HTML; tabbed content is fully exposed). Examples: /reference.md, /guides/v2/payment-widget.md.
Recommended sequence when using this page:
- Read this page to build the mental model.
- Determine: SDK version → product → payment scenario.
- For details beyond this page, consult llms.txt → navigate to the precise guide/reference page.
- Generate implementation code.
- Validate against §8 Common Mistakes before finalizing.
- Prefer V2 SDK unless the user specifies V1 or shows existing V1 code.
- Prefer the checkout-page type when no specific UI/scenario keyword is given.
- Ask a brief clarification question only when ambiguity affects architecture (e.g., recurring vs one-time, custom UI vs default). Do not ask for cosmetic choices.
Server-authoritative model — Toss Payments follows this core principle:
- Client may initiate payment authentication via the JS SDK.
- Server must validate and finalize the payment state via the secret key (confirm API, billing API, refund API, webhook re-query).
- Never trust client-provided payment state (amount, status, success). Always re-verify on the server.
This underlies amount verification (§2.2), confirm API (§5.A~C), billing flow (§5.D), and webhook handling (§7.2).
Toss Payments uses 3 actors: Client (browser) / Merchant Server / Toss Payments. The diagram below shows the basic flow for one-time payments (card, transfer, easy pay). Virtual account and auto-billing are variations of this flow.
In short:
- Client requests payment authentication via SDK → user authenticates at Toss Payments.
- Server confirms the authenticated payment with the secret key, then saves the result.
Responsibility split:
| Step | Actor | Key used |
|---|---|---|
Payment request (requestPayment) | Client (JS SDK) | Client key |
| amount verification | Merchant server | — |
Payment confirmation (/v1/payments/confirm) | Merchant server | Secret key |
| Webhook handling | Merchant server | — |
Scenario variations:
- One-time payment (card / transfer / easy pay): Flow above as is.
- Virtual account: Step 3 only issues an account number. Payment is finalized asynchronously via
DEPOSIT_CALLBACKwebhook after deposit. - Auto-billing: Billing key issuance (SDK method — variation of steps 2~4 / API method — server-only, separate contract required) → server calls billing endpoint for recurring payments.
4 mandatory rules before integration. Violation leads to security incidents, payment tampering, or hallucinated code.
| Key | Pattern | Used in |
|---|---|---|
| Secret key | (test|live)_sk_* | Server |
| Client key | (test|live)_ck_* | Client (browser-exposed), SDK initialization |
Never expose secret key — Including it in frontend code, GitHub pushes, or client responses leads to immediate security incidents.
For key types and issuance, see API Keys Guide.
requestPayment runs in the client JS SDK, so a user can modify amount arbitrarily from the browser console. (e.g., the order shows 50,000 KRW but the console call is changed to amount: 100.) Authentication completes with the modified amount and the browser redirects via successUrl. If the merchant passes that value to the confirm API without verification, the payment completes at the tampered amount.
(Modifying the successUrl query string directly is blocked by the confirm API since it differs from the authenticated amount. The threat is tampering before authentication.)
Verify in the server that amount from successUrl matches the merchant's intended amount at request time before calling the Payment Confirmation API.
LLM authoring guideline — Even in toy/PoC code without a DB, leave a verification placeholder and inform the user via comment or response: "In production, must compare with server-stored amount."
Append a single colon to the secret key, then base64-encode. Missing colon is the most common mistake.
- The password slot after the colon is empty (hence trailing colon).
- If UTF-8 BOM is present, the result starts with
77u/. Re-encode without BOM.
For details, see Authorization Header.
LLMs frequently hallucinate undocumented or deprecated fields when generating PG integration code. Use only fields defined in this page or in the linked SDK/API docs. If a field is not documented here or in linked docs, do not generate it.
- Do not infer field names from other PG vendors (e.g., Stripe's
payment_intent, KCP, INICIS). - Do not generate fields based on naming patterns alone (e.g., guessing
payment_method_idwhen onlymethodis documented). - If a needed field is unclear, consult llms.txt → exact page, or ask the user. Do not guess.
- V2 by default — If user does not specify V1, use V2.
- Do not mix V1 and V2 — Common mistake: calling V2 methods on V1 SDK URL.
- If existing code is V1, confirm with user before keeping V1.
Toss Payments SDK hierarchy:
SDK version is independent of API version — both V1 and V2 SDKs use api.tosspayments.com/v1/*.
| Item | V1 | V2 |
|---|---|---|
| JS SDK URL | Per product (e.g., https://js.tosspayments.com/v1/payment-widget) | Single (https://js.tosspayments.com/v2/standard) |
| SDK init | Per product (PaymentWidget(), BrandPay(), TossPayments()) | One line TossPayments(clientKey), branched by methods |
V2 SDK initialization and product methods:
widgets() covers two types that differ only in the render method. payment() is the legacy payment-window SDK, also used to open a card company's own window.
For differences, see Migration Guide.
- If the user specifies a product, use it.
- If not specified, branch by user requirement keyword (table below).
- If no keyword matches, proceed with the checkout-page type (default). Add a note at the end of the response: "If you want the payment UI in a popup window instead of the order page, switch to the payment-window type."
- If keywords match two or more products, briefly confirm with the user.
| Keyword | Product | V2 entry point | Guide |
|---|---|---|---|
| (Vague) "payment", "payment integration" | Checkout-page type / 주문서형 결제 (default) | widgets() + renderPaymentMethods() | ↗ |
| "payment window", "popup", "open in a window" | Payment-window type / 결제창형 결제 | widgets() + renderPaymentWindow() | ↗ |
| "card company's own window", "app card", "direct" | Direct type / 자체창 | payment() + card.flowMode: "DIRECT" | ↗ |
| "one-click", "password payment", "build own easy pay" | BrandPay / 브랜드페이 | tossPayments.brandpay() | ↗ |
| "subscription", "recurring", "billing" | Auto-billing / 자동결제(빌링) | tossPayments.payment() (billing) | ↗ |
| "seller settlement", "marketplace payout" | Payouts / 지급대행 | API direct (no SDK) | ↗ |
When searching docs, use the Korean name. The English type names exist only inside this page. Guide pages are written in Korean and the search index matches on substrings, so an English type name returns nothing — query 주문서형 결제, not checkout-page type.
The legacy payment window / 결제창(구버전) — tossPayments.payment() with its own guide — still exists for merchants integrated before the current types. Do not choose it for a new integration unless the user explicitly asks for it.
For full comparison, see Payment Products Comparison.
Docs, blog posts, and LLM training data written before the 2026 renaming use old product names. Map them to the current products:
| Legacy name | Current product |
|---|---|
| 결제위젯 (payment widget) | 주문서형 결제 (checkout-page type) · 결제창형 결제 (payment-window type) |
| 결제위젯(주문서형) | 주문서형 결제 |
| 결제위젯(결제창형) | 결제창형 결제 |
| 통합결제창 | 결제창(구버전)의 카드·간편결제 결제창 — legacy, not for new integrations |
When the user says "결제위젯" or "통합결제창", treat it as the mapped current product and mention that the name changed.
All three are payment-window types and all are current V2. They differ in where the window appears — embedded in the order page, opened as a popup, or handed off to the card company's own app.
- Checkout-page type (주문서형 결제) — SDK renders the payment method UI inside the order page. Merchant provides container divs.
widgets()→setAmount()→renderPaymentMethods()→renderAgreement()→ buyer clicks pay →requestPayment() - Payment-window type (결제창형 결제) — SDK renders an entry that opens the payment window as a popup.
widgets()→setAmount()→renderPaymentWindow()→paymentRequestevent →requestPayment() - Direct type (자체창) — Opens the card company's or easy-pay app's own window directly. There is no dedicated SDK; it runs on the legacy payment-window SDK.
payment()→requestPayment({ card: { flowMode: "DIRECT" } })
The first two share tossPayments.widgets() and differ only in the render method. Only the direct type uses tossPayments.payment().
If no specific clue, choose the checkout-page type.
Each scenario's V2 entry point, flow, and full-code routing. Look up full code in the guide pages — this page provides entry points only.
Used across all scenarios. Do not fill arbitrarily — follow format constraints.
| Identifier | Issued by | Format |
|---|---|---|
customerKey | Merchant | 2~300 chars. Letters, digits, and special chars (-, _, =, ., @). At least one special char required. UUID recommended. Do not use predictable values (email, phone, member ID). For non-members, ANONYMOUS is allowed. |
orderId | Merchant | 6~64 chars. Letters, digits, -_. Unique per payment. |
paymentKey | Toss Payments | Received via successUrl after payment authentication. Payment identifier. |
billingKey | Toss Payments | Received after billing key issuance (scenario 5.D). Recurring payment token. |
| Field | V2 SDK call | V1 SDK / Server API |
|---|---|---|
amount | Object: { value: integer, currency: "KRW" } | Integer (e.g., 50000) |
customerKey, orderId, orderName | string | string |
paymentKey, billingKey | string | string |
Idempotency-Key (header) | UUID v4 string | UUID v4 string |
amount.currency is "KRW" for domestic payments. Overseas easy pay (PayPal) supports "USD" only. amount.value is an integer for KRW (no decimals).
After client requestPayment, the auth result redirects to one of these URLs. Branch in merchant server.
- successUrl query string (auth success):
paymentKey,orderId,amount(the checkout-page type also includespaymentType) → verify amount (Critical 2.2) → call confirm API. - failUrl query string (auth failure / user cancellation):
code,message,orderId→ do not call confirm API. Show a user-friendly message based oncode, then offer a retry path (e.g., back to checkout). Do not auto-redirect to a generic error page — that loses context. For codes, see SDK Error Codes.
Scenarios 5.A·5.B·5.C require calling the confirm API on the server after the client payment request to finalize. 5.D (billing) and 5.E (cancel) are separate flows.
Use the merchant's intended amount at request time for amount, not the successUrl query (Critical 2.2).
- Entry point:
tossPayments.widgets({ customerKey }) - Flow:
setAmount({ value, currency: "KRW" })→renderPaymentMethods→renderAgreement→requestPayment({ orderId, orderName, successUrl, failUrl })→ successUrl → server confirm - Key: SDK auto-renders payment method/agreement UI inside the order page. Merchant only provides container divs.
- Full code: Checkout-page type Guide, SDK Reference
Canonical flow (baseline pattern for any one-time payment — use this as the default mental model):
- Entry point:
tossPayments.widgets({ customerKey })— same as 5.A; only the render method differs - Flow:
setAmount({ value, currency: "KRW" })→renderPaymentWindow→paymentRequestevent →requestPayment→ successUrl → server confirm - Key: SDK renders an entry that opens the payment window as a popup instead of embedding the UI in the order page.
- Full code: Payment-window type Guide, SDK Reference
Legacy payment window — merchants integrated before the current types call tossPayments.payment({ customerKey }).requestPayment({ method, amount: { value, currency: "KRW" }, orderId, orderName, successUrl, failUrl, ... }) in a single client call and branch by method (CARD, VIRTUAL_ACCOUNT, TRANSFER, MOBILE_PHONE, EASY_PAY, CULTURE_GIFT_CERTIFICATE, etc.). Scenario 5.C and the direct type run on this SDK. Guide: legacy payment window, SDK Reference. Do not pick it for a new integration.
- Entry point:
tossPayments.payment().requestPayment({ method: "VIRTUAL_ACCOUNT", amount: { value, currency: "KRW" }, virtualAccount, orderId, orderName, successUrl, failUrl, customerEmail?, customerName?, customerMobilePhone? }) virtualAccountfields: One ofvalidHoursordueDateis required.cashReceipt,escrowProductsare optional.- Customer info:
customerEmail/customerName/customerMobilePhoneare optional in SDK but practically needed for deposit notification and refund. Recommended to collect. - Flow: Issue → confirm API call (response
status: WAITING_FOR_DEPOSIT) → wait for deposit →DEPOSIT_CALLBACKwebhook → query API → confirmstatus: DONE→ mark as completed - Key: Request time ≠ completion time. Do not mark as completed on issue response.
DEPOSIT_CALLBACKwebhook handling required. - Full code: Virtual Account Glossary, Webhook Events
Billing key issuance → server calls billing endpoint for recurring payments. Two methods for billing key issuance.
SDK method (default):
- Step 1 (client):
payment.requestBillingAuth({ method: "CARD" })→ successUrl receivescustomerKey,authKey - Step 2 (server):
POST /v1/billing/authorizations/issue(authKey→billingKey) - Step 3 (server):
POST /v1/billing/{billingKey}(recurring payment)
API method (separate contract required — non-authenticated payment qualification + PCI-DSS):
- Server:
POST /v1/billing/authorizations/card(card info directly) →billingKey - Recurring payment with billing key is the same as SDK step 3
Key: Billing key payment does NOT use requestPayment/successUrl//v1/payments/confirm flow. Server-only.
Full code: Auto-billing Guide, Billing API
- Cancel:
POST /v1/payments/{paymentKey}/cancel+{ cancelReason } - Partial cancel: Add
cancelAmountto body - Virtual account refund: Add
refundReceiveAccount(bank/account number/holder name) to body - Query:
GET /v1/payments/{paymentKey}orGET /v1/payments/orders/{orderId} - Full code: Cancel API, Query API
Even after the scenario (5.A~5.E) is set, each payment method has caveats. Flow mapping is in section 5; this section is pitfalls only.
| Payment method | Coding caveat |
|---|---|
| Virtual account | DEPOSIT_CALLBACK webhook handling required. Do not mark completed on issue response. (Scenario 5.C) |
| Easy pay | KakaoPay testable only with contracted MID key. Payco testable only with live key. Apple Pay shown only on iPhone/Mac Safari. |
| Transfer | Quick Account Transfer and BankPay differ in identity verification and cancellation policy. |
| Mobile / Gift certificate | Per-method limit policies vary. |
| Non-authenticated payment | Payment with card info only (number, expiry, DOB, password) without identity verification. Separate merchant contract required. |
| Auto-billing | Billing key payment is server-only (does NOT use requestPayment/successUrl//v1/payments/confirm flow). (Scenario 5.D) |
Key terms:
- Transaction — Each approve/cancel/partial-cancel from a payment is a "transaction". Identified by
transactionKey. - Settlement — Paid out by transaction date + business day. General/scheduled settlement.
For payment method policies: Card, Virtual Account, Transfer, Easy Pay, Non-auth Payment, Auto-billing, Transaction, Settlement.
- Available on all POST APIs (GET ignored).
- Max 300 chars, UUID v4 recommended.
- Valid for 15 days from first request.
- Same idempotency key returns the first response (prevents duplicate payments).
For details, see Auth & Headers.
| Webhook type | Event examples | Verification |
|---|---|---|
| General payment | PAYMENT_STATUS_CHANGED, DEPOSIT_CALLBACK, CANCEL_STATUS_CHANGED | No signature header. Re-call Payment Query API with paymentKey to verify status. |
| Payouts/Seller | payout.changed, seller.changed | Verify tosspayments-webhook-signature header with HMAC SHA-256. |
For details, see Webhook Events.
- Test environment: Key prefix
test_*. No real deposit. Virtual account number prefixed withX. KakaoPay/Payco have separate policies. - Live environment: Key prefix
live_*. Real deposits.
For details, see Environment Setup.
Frequently wrong patterns when LLMs generate Toss Payments code.
| Wrong pattern | Correct pattern |
|---|---|
Authorization: Basic base64(SECRET_KEY) (no colon) | Authorization: Basic base64(SECRET_KEY:) (colon required) |
Pass successUrl amount directly to confirm API | Compare with stored order amount first, then pass |
Call server API (api.tosspayments.com/*) with client key | Use secret key for server APIs |
Hardcode secret key without .env, push to GitHub | Use environment variable, add to .gitignore |
Call V2 methods on V1 SDK URL (/v1/payment-widget) | Use https://js.tosspayments.com/v2/standard for V2 |
Call updateAmount() in the V2 web checkout-page type | Use setAmount() in the V2 web SDK (updateAmount remains in the V2 mobile SDKs) |
requestPayment("CARD", {...}) (V1 signature) in V2 Payment Window | requestPayment({ method: "CARD", amount: {...}, ... }) (V2 unified signature) |
amount: 15000 (number) in V2 BrandPay | amount: { value: 15000, currency: "KRW" } (object) |
| Mark virtual account as completed on issue response | Confirm via DEPOSIT_CALLBACK webhook or query API |
Implement tosspayments-webhook-signature for general payment webhook | General payment webhook has no signature header. Re-verify via Query API with paymentKey |
| Add Idempotency-Key to GET request | Idempotency-Key applies only to POST (ignored on GET) |
| Retry idempotent error by changing only the key | Inspect the error first before retry (risky) |
| Manage test/live keys in the same env variable | Separate by test_*/live_* prefix |
Pass amount field as a string | amount.value is integer, amount.currency is string ("KRW") |
Use requestPayment flow after billing key issuance | Billing key payment is server-only (/v1/billing/{billingKey}) |
| Treat confirm API failure as immediate payment failure | Check status with Query API before branching (may be already authenticated/approved) |
Call confirm API after failUrl redirect | failUrl is auth failure/cancellation. Do not call confirm API; show message by code |
Use email/phone/member ID directly as customerKey | Use unpredictable values (UUID; 2~300 chars; at least one special char). For non-members, ANONYMOUS |
Generate orderId in arbitrary format (whitespace/non-ASCII/etc.) | Letters/digits/-_, 6~64 chars |
Generate customerKey with 1 char (e.g., "a") | Min 2 chars, must include at least one special char (-_=.@) |
Scenario-specific full-code routing is in section 5 (entry points). This section is for meta/operations/reference information beyond scenarios.
| What you need | Page |
|---|---|
| Full API spec | API Reference |
| SDK setup / init | SDK Environment |
| Checkout-page type | SDK Reference |
| Payment-window type | SDK Reference |
| Direct type | SDK Reference |
| BrandPay SDK | SDK Reference |
| SDK error codes | Error Codes |
| Cancel/refund details | Cancel API |
| Payment query details | Query API |
| Webhook event spec | Webhook Events |
| Auth header details | Authorization Header |
| What you need | Page |
|---|---|
| Environment setup (test/live) | Environment Setup |
| API key issuance/management | API Keys Guide |
| Payment flow for humans | Payment Flow Guide |
| What you need | Page |
|---|---|
| Full docs index (LLM standard format) — primary routing resource | llms.txt |
| MCP server·LLM integration tools | LLMs Integration Guide |