TossPayments Integration
Accept Korean payment methods including credit/debit cards, bank transfers, virtual accounts, and mobile payments using TossPayments.Prerequisites
Before you begin, make sure you have:- A TossPayments account — sign up at tosspayments.com if you don’t have one
- Your API keys from the TossPayments Developer Center
- A working Headless Commerce store with at least one product
Use test mode keys during development. Test keys use the
test_ck_ and test_sk_ prefixes. Switch to live keys only when you’re ready to accept real payments.Environment Variables
Add the following environment variables to your API server:Checkout Flow
The TossPayments checkout flow involves four steps: creating a cart, initiating checkout, completing payment with the TossPayments Widget SDK, and confirming the payment server-side.Call the checkout endpoint with
payment_method: 'tosspayments'. The API returns toss_payment data needed to render the TossPayments widget:Use the TossPayments Widget SDK on your frontend to display the payment UI and let the customer complete payment:
import { loadTossPayments } from '@tosspayments/tosspayments-sdk';
const tossPayments = await loadTossPayments(
process.env.NEXT_PUBLIC_TOSS_CLIENT_KEY!
);
const widgets = tossPayments.widgets({ customerKey: 'guest' });
// Set the payment amount
await widgets.setAmount({
currency: 'KRW',
value: order.toss_payment.amount,
});
// Render the payment method selector
await widgets.renderPaymentMethods({
selector: '#payment-method',
variantKey: 'DEFAULT',
});
// Render the terms agreement
await widgets.renderAgreement({
selector: '#agreement',
variantKey: 'AGREEMENT',
});
// When the customer clicks "Pay", request payment
async function handlePayment() {
try {
await widgets.requestPayment({
orderId: order.toss_payment.order_id,
orderName: order.toss_payment.order_name,
successUrl: `${window.location.origin}/payment/success`,
failUrl: `${window.location.origin}/payment/fail`,
});
} catch (error) {
// User cancelled or an error occurred
console.error(error);
}
}
import { useEffect, useRef } from 'react';
import { loadTossPayments } from '@tosspayments/tosspayments-sdk';
interface PaymentWidgetProps {
orderId: string;
orderName: string;
amount: number;
}
function TossPaymentWidget({ orderId, orderName, amount }: PaymentWidgetProps) {
const widgetsRef = useRef<ReturnType<
Awaited<ReturnType<typeof loadTossPayments>>['widgets']
> | null>(null);
useEffect(() => {
async function initWidgets() {
const tossPayments = await loadTossPayments(
process.env.NEXT_PUBLIC_TOSS_CLIENT_KEY!
);
const widgets = tossPayments.widgets({ customerKey: 'guest' });
await widgets.setAmount({ currency: 'KRW', value: amount });
await widgets.renderPaymentMethods({
selector: '#payment-method',
variantKey: 'DEFAULT',
});
await widgets.renderAgreement({
selector: '#agreement',
variantKey: 'AGREEMENT',
});
widgetsRef.current = widgets;
}
initWidgets();
}, [amount]);
const handlePayment = async () => {
if (!widgetsRef.current) return;
await widgetsRef.current.requestPayment({
orderId,
orderName,
successUrl: `${window.location.origin}/payment/success`,
failUrl: `${window.location.origin}/payment/fail`,
});
};
return (
<div>
<div id="payment-method" />
<div id="agreement" />
<button onClick={handlePayment}>결제하기</button>
</div>
);
}
After the customer completes payment, TossPayments redirects to your
successUrl with paymentKey, orderId, and amount as query parameters. You must confirm the payment by calling the Headless Commerce confirm endpoint:Webhook Setup
Headless Commerce listens for TossPayments webhook events to handle asynchronous payment updates (e.g., virtual account deposits, cancellations). Configure webhooks in the TossPayments Developer Center:- Go to Developer Center → Webhook Settings
- Enter your webhook URL:
https://api.headlesscommerce.io/v1/webhooks/tosspayments - Select the events to receive
- Save the Webhook secret and set it as
TOSS_WEBHOOK_SECRET
Webhook Signature Verification
TossPayments signs webhook payloads using HMAC-SHA256. The signature is included in thex-tosspayments-signature header.
Headless Commerce verifies this automatically, but if you need to implement custom verification:
Event Types
| TossPayments Event | Headless Commerce Action |
|---|---|
PAYMENT_STATUS_CHANGED (status: DONE) | Order status set to confirmed, payment marked completed |
PAYMENT_STATUS_CHANGED (status: CANCELED) | Order cancelled, payment.refunded webhook sent |
PAYMENT_STATUS_CHANGED (status: PARTIAL_CANCELED) | Partial refund recorded |
VIRTUAL_ACCOUNT_DEPOSIT | Virtual account payment confirmed, order marked confirmed |
Testing
Test mode keys
Use test mode API keys during development. Test transactions are simulated and do not process real payments.| Key type | Prefix | Example |
|---|---|---|
| Client key | test_ck_ | test_ck_D5GePWvyJ... |
| Secret key | test_sk_ | test_sk_zXLkKEypN... |
Test payments
In test mode, the TossPayments widget displays simulated payment flows:- Credit card: Select any card brand and complete the simulated authentication
- Bank transfer: The transfer is instantly simulated as successful
- Virtual account: A test virtual account is created and you can simulate a deposit
- Mobile payment: Simulated mobile payment flow
TossPayments test mode does not use specific test card numbers like Stripe. Instead, the entire payment widget operates in a sandbox environment where all payment methods are simulated.
Testing webhooks locally
Use a tunneling tool like ngrok to forward webhook events to your local server:Error Handling
Handle payment errors in both the checkout and confirmation steps: Checkout errors:Handling the failure redirect
When a payment fails, TossPayments redirects to yourfailUrl with error details:
| Error Code | Description | Suggested Action |
|---|---|---|
PAY_PROCESS_CANCELED | User cancelled the payment | Allow the user to retry |
PAY_PROCESS_ABORTED | Payment process was aborted | Allow the user to retry |
REJECT_CARD_COMPANY | Card company rejected the payment | Ask to use a different card |
EXCEED_MAX_DAILY_PAYMENT_COUNT | Daily payment limit exceeded | Try again the next day |
EXCEED_MAX_PAYMENT_AMOUNT | Maximum payment amount exceeded | Reduce the order amount |
INVALID_CARD_EXPIRATION | Invalid card expiration date | Ask to re-enter card details |
NOT_SUPPORTED_INSTALLMENT_PLAN | Installment plan not supported | Choose a different installment option |
Next Steps
Webhooks
Learn about all webhook events and payload formats.
Stripe
Set up Stripe for global payment methods.
API Reference
Explore all checkout and payment endpoints.
SDKs
Full SDK reference with every resource and method.