Toss Payments Integration Guide
Integrate Toss Payments: widget vs API keys, the confirm call and its 10-minute limit, test and live keys, webhooks, refunds and the mistakes to avoid.
To integrate Toss Payments, render its payment widget in the browser and request a payment, then have your server check the returned amount and call the confirm API with your secret key within 10 minutes. Webhooks, cancels and the switch from test to live keys complete the integration.
This guide is for developers, including those outside Korea, who are adding Toss Payments to a site or app. It follows the official documentation (links at the end, checked October 2026) and points out the places where integrations usually go wrong. Code is shown in short fragments. Use the official samples for complete files.
What is Toss Payments?
Toss Payments is a Korean payment gateway (PG). It runs the checkout for cards, bank transfer, virtual accounts, mobile phone billing and easy-pay wallets. Its documentation lists Toss Pay, Kakao Pay, Naver Pay, PAYCO, Samsung Pay, Apple Pay, L.Pay and SSG Pay as supported easy pays. Like other domestic PGs, it signs merchants through an electronic payment application that asks for a Korean business registration number. If you are still choosing between a domestic PG and a global processor, start with our overview of Korean payment gateway integration.
The developer documentation lives at docs.tosspayments.com. Most of it is in Korean. There is an English section (docs.tosspayments.com/en) covering the integration guides and the core API, though parts of it are still labelled in Korean.
Should you use the payment widget or the API keys?
Toss Payments issues two sets of keys, and you must use them as matched pairs. The widget keys start with gck and gsk. The API individual keys start with ck and sk. Each set has a test and a live version, so a widget client key in test mode starts with test_gck and its secret partner starts with test_gsk. If you mix sets, or mix test with live, the API returns INVALID_API_KEY.
| Payment widget (order-form type) | API individual keys | |
|---|---|---|
| Client key prefix | test_gck / live_gck | test_ck / live_ck |
| Secret key prefix | test_gsk / live_gsk | test_sk / live_sk |
| What you get | Toss renders the payment method list and terms UI inside your page; you can define several payment UI variants and pick one with variantKey | Keys for the older payment window and BrandPay |
| When it fits | Most new web checkouts | Existing integrations on the older payment window, or BrandPay |
For a standard web shop or booking flow, start with the widget. The v2 SDK merges what used to be separate SDKs into one, and the widget guide is the most complete walkthrough in the current documentation.
How does the payment flow work?
The widget flow has four client steps, then a server step. The SDK loads from https://js.tosspayments.com/v2/standard or the npm package @tosspayments/tosspayments-sdk.
- Initialise:
const tossPayments = TossPayments(clientKey)and thenconst widgets = tossPayments.widgets({ customerKey }). For guests, passTossPayments.ANONYMOUSas the customer key. - Set the amount before anything else renders:
await widgets.setAmount({ currency: "KRW", value: 15000 }). - Render the UI:
widgets.renderPaymentMethods({ selector: "#payment-method", variantKey: "DEFAULT" })andwidgets.renderAgreement({ selector: "#agreement", variantKey: "AGREEMENT" }). - Request the payment after the UI has rendered:
await widgets.requestPayment({ orderId, orderName, successUrl, failUrl }). - On your
successUrl, read the query parameters and confirm the payment from your server.
Toss's own sample code adds one instruction that is easy to skip. Save the orderId and amount on your server before you call requestPayment. The success redirect is just a URL in the customer's browser, and anyone can edit it. The stored order is your source of truth.
How do you confirm a payment on the server?
The success redirect brings back paymentKey, orderId, amount and paymentType. Toss tells you to check that the returned amount matches what you set. A mismatch can mean someone tampered with the request. Compare it with the amount stored for that orderId, not with anything the browser sends.
Then call the confirm API:
- Endpoint:
POST https://api.tosspayments.com/v1/payments/confirm - Header:
Authorization: Basicfollowed by the Base64 of your secret key with a colon appended, for exampleBuffer.from(secretKey + ":").toString("base64")in Node.js. The colon is there because the secret key is the username and the password is empty. - Body:
{ "paymentKey": "...", "orderId": "...", "amount": 15000 }
You have 10 minutes. If you confirm more than 10 minutes after the payment request, the payment data is gone and you get NOT_FOUND_PAYMENT_SESSION. In practice, that means the confirm call belongs in the request that handles the success redirect, not in a background job that might be queued.
Two more rules from the documentation. The secret key used to confirm must belong to the same set as the client key that requested the payment. Payment is only taken when the confirm call succeeds, so the customer's card is not charged when the widget closes.
How do test keys and going live work?
Keys that start with test_ run in test mode. Payments made with them are not real, and they show up under test payment history in the developer center. The documentation pages include shared documentation test keys you can use before you have an account. Once the electronic payment contract is in place, you get test keys for your own merchant ID (MID). Use those for final testing, because some behaviour depends on the settings of your MID.
Going live means swapping in the matching live_ keys and nothing else in the code. That is why keys belong in environment variables, never in source. The widget keys only appear after the electronic payment application, so start the contract early. It runs through document review and card company review, and that timeline does not depend on how fast the code gets written.
How should you handle webhooks?
Register webhooks in the developer center: a name, your endpoint URL and the events you want. Configuration is per MID. The events most integrations need are:
PAYMENT_STATUS_CHANGED: payment status changes, for every payment method.DEPOSIT_CALLBACK: virtual account deposits and their cancellation.CANCEL_STATUS_CHANGED: status of cancellations.
Your endpoint must answer with HTTP 200 within 10 seconds. If it does not, Toss retries up to seven times over roughly three days and 19 hours, with gaps growing from 1 minute to 4,096 minutes. The event is marked as failed after the seventh failure.
The webhook guide describes no signature for these payment events. Treat the payload as a hint, not as proof. When a webhook arrives, look the payment up yourself with GET /v1/payments/{paymentKey} (or GET /v1/payments/orders/{orderId}) using your secret key, and act on that response. Make the handler idempotent, because retries mean you will see the same event more than once.
How do refunds and partial cancels work?
Cancel with POST /v1/payments/{paymentKey}/cancel. A cancelReason is required (up to 200 characters). Add cancelAmount for a partial cancel and leave it out to cancel the full amount. Cancelling a virtual account payment also needs a refundReceiveAccount object with the bank, account number and holder name, because the money goes back to a bank account rather than a card.
Send an Idempotency-Key header on cancels. It is a unique random value of up to 300 characters, valid for 15 days, and it stops a network retry from cancelling twice. Generate it when the refund is created in your system and store it with the refund, so a retry reuses the same key.
What mistakes come up most often?
- Mixing widget keys with API keys, or test with live, which returns
INVALID_API_KEY. - Trusting the
amountin the success URL instead of the amount stored for the order. - Confirming late, for example from a queue, and hitting the 10-minute limit.
- Calling
requestPaymentbeforerenderPaymentMethodshas finished, which stops the selected payment window from opening. - Marking orders paid straight from a webhook payload without looking the payment up.
- Forgetting that virtual account payments complete later, when the customer actually transfers the money.
- Leaving refunds to manual work in the admin console instead of building them into your own back office.
What we've seen building for Korea
The billing in our own client portal runs on PayApp, a different Korean payment service, but the lessons carry over to Toss. Keep the decision "record this payment or not" in one tested function rather than spread through the route handler, and verify the request before you look at the payment status. Each notification should end up as paid, cancelled (including partial cancels, which reverse what you recorded) or ignored, for states such as a pending request. An amount that cannot be parsed should become "unknown", never zero, because a zero would quietly record a 0 KRW payment.
The same principles apply to a Toss integration. Save the order before the redirect, confirm from the server, look the payment up when a webhook arrives, and design refunds before launch, not after the first customer asks for one.
Our public estimator lists a one-time payment module at 800,000 to 1,800,000 KRW and recurring billing at 2,000,000 to 4,000,000 KRW. Both exclude 10% VAT, and PG fees are separate (pricing catalogue v1.1, 23 September 2026).
Frequently asked questions
What is a Toss payment?
It usually means a payment processed by Toss Payments, the payment gateway, which runs checkouts for Korean merchants. It can also mean Toss Pay, the easy-pay wallet inside the Toss app, which merchants can offer as one of the easy-pay options in the Toss Payments checkout.
Is there an English version of the Toss Payments developer docs?
Yes. docs.tosspayments.com/en covers the integration guides and the core payment APIs. Parts of it are still labelled in Korean, so keep the Korean pages open for newer features.
How long do I have to confirm a Toss payment?
10 minutes from the payment request. After that the payment session is gone and the confirm API returns NOT_FOUND_PAYMENT_SESSION.
Are test payments charged?
No. Payments made with test_ keys are not real and appear only in the developer center's test payment history.
Can a company outside Korea use Toss Payments?
The electronic payment application asks for a Korean business registration number, so you need a Korean entity or a Korean partner who holds the contract. Without one, look at a global processor that supports Korean payment methods.
How do I verify a Toss Payments webhook?
The webhook guide describes no signature for payment events, so look up the payment with the payments API using your secret key and act on that response, not on the webhook body.
Need Toss Payments wired into your product?
If you have a Toss Payments contract, or are about to apply, and need the checkout, confirmation, webhooks and refunds built properly, send us your requirements. Our web development page shows the kind of services we build. For the rest of a Korean launch, see KakaoTalk Channel and AlimTalk for order notifications, Kakao and Naver login for sign-in and the Korea market entry overview.
Sources
All checked October 2026.
- Toss Payments: payment widget integration guide (Korean)
- Toss Payments: widget integration (English)
- Toss Payments: Payment APIs (English)
- Toss Payments: API keys (Korean)
- Toss Payments: core API reference (Korean)
- Toss Payments: webhooks (Korean)
- Toss Payments: easy pay methods (Korean)
- Toss Payments blog: checking API keys for integration (Korean)
- Toss Payments official sample code (GitHub)
- PayApp developer documentation (Korean)

