토스페이먼츠 완전 연동 가이드: 결제부터 환불 자동화까지
토스페이먼츠는 국내 핀테크 시장에서 가장 널리 쓰이는 결제 솔루션 중 하나입니다. 개발자 친화적인 API 문서와 SDK를 제공하며, 카드·계좌이체·간편결제 등 다양한 결제 수단을 지원합니다. 이 글에서는 토스페이먼츠를 처음 연동하는 개발자를 위해 결제창 띄우기부터 결제 승인, 그리고 웹훅을 이용한 환불 자동화까지 전 과정을 실전 코드와 함께 안내합니다.
1. 사전 준비
토스페이먼츠 개발자센터(developers.tosspayments.com)에 가입하면 테스트용 클라이언트 키와 시크릿 키를 발급받을 수 있습니다. 실제 결제는 라이브 키로 전환해야 하며, 테스트 환경에서는 실제 출금이 발생하지 않습니다.
| 구분 | 클라이언트 키 | 시크릿 키 |
|---|---|---|
| 테스트 | test_ck_... | test_sk_... |
| 라이브 | live_ck_... | live_sk_... |
| 용도 | 프론트엔드 SDK 초기화 | 서버 API 인증 (Base64 인코딩) |
| 주의 | 공개 가능 | 절대 노출 금지 |
2. 프론트엔드: 결제창 띄우기 (SDK v2)
토스페이먼츠는 SDK v2 사용을 권장합니다. CDN 방식으로 간단하게 로드할 수 있습니다.
<!-- HTML head에 추가 -->
<script src="https://js.tosspayments.com/v2/standard"></script>
결제 위젯을 초기화하고 결제창을 호출하는 JavaScript 코드입니다.
const clientKey = "test_ck_D5GePWvyJnrK0W0k6q8gLzN97E0R";
const tossPayments = TossPayments(clientKey);
const widgets = tossPayments.widgets({ customerKey: TossPayments.ANONYMOUS });
async function renderPaymentWidget() {
await widgets.setAmount({ currency: "KRW", value: 15000 });
await Promise.all([
widgets.renderPaymentMethods({ selector: "#payment-method", variantKey: "DEFAULT" }),
widgets.renderAgreement({ selector: "#agreement", variantKey: "AGREEMENT" }),
]);
}
async function requestPayment() {
await widgets.requestPayment({
orderId: "order_" + Date.now(),
orderName: "코드픽 프리미엄 구독",
successUrl: window.location.origin + "/payment/success",
failUrl: window.location.origin + "/payment/fail",
customerEmail: "user@example.com",
customerName: "홍길동",
});
}
3. 백엔드: 결제 승인 처리 (FastAPI)
결제창에서 결제가 완료되면 successUrl로 리다이렉트되며, 쿼리 파라미터로 paymentKey, orderId, amount가 전달됩니다. 이를 받아 토스페이먼츠 서버에 승인 요청을 보내야 실제 결제가 확정됩니다.
import httpx
import base64
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
router = APIRouter()
SECRET_KEY = "test_sk_zXLkKEypNArWmo50nX3lmeaxYG5R"
def get_auth_header() -> str:
encoded = base64.b64encode(f"{SECRET_KEY}:".encode()).decode()
return f"Basic {encoded}"
class PaymentConfirmRequest(BaseModel):
paymentKey: str
orderId: str
amount: int
@router.post("/payment/confirm")
async def confirm_payment(body: PaymentConfirmRequest):
async with httpx.AsyncClient() as client:
response = await client.post(
"https://api.tosspayments.com/v1/payments/confirm",
headers={
"Authorization": get_auth_header(),
"Content-Type": "application/json",
},
json={
"paymentKey": body.paymentKey,
"orderId": body.orderId,
"amount": body.amount,
},
)
if response.status_code != 200:
raise HTTPException(status_code=400, detail=response.json())
return response.json()
4. 환불(결제 취소) API 연동
환불은 POST /v1/payments/{paymentKey}/cancel 엔드포인트를 사용합니다. 전액 환불 또는 부분 환불 모두 지원합니다.
@router.post("/payment/cancel")
async def cancel_payment(payment_key: str, cancel_reason: str, cancel_amount: int = None):
body = {"cancelReason": cancel_reason}
if cancel_amount:
body["cancelAmount"] = cancel_amount
async with httpx.AsyncClient() as client:
response = await client.post(
f"https://api.tosspayments.com/v1/payments/{payment_key}/cancel",
headers={
"Authorization": get_auth_header(),
"Content-Type": "application/json",
},
json=body,
)
if response.status_code != 200:
raise HTTPException(status_code=400, detail=response.json())
return response.json()
5. 웹훅(Webhook)으로 환불 자동화하기
웹훅을 활용하면 결제 상태 변경을 실시간으로 감지해 DB 업데이트, 이메일 발송 등의 후속 작업을 자동화할 수 있습니다. 토스페이먼츠는 결제 취소 시 PAYMENT_STATUS_CHANGED 이벤트를 상점 서버로 전송합니다.
from fastapi import Request
import logging
logger = logging.getLogger(__name__)
@router.post("/webhook/tosspayments")
async def toss_webhook(request: Request):
payload = await request.json()
event_type = payload.get("eventType")
data = payload.get("data", {})
if event_type == "PAYMENT_STATUS_CHANGED":
status = data.get("status")
order_id = data.get("orderId")
payment_key = data.get("paymentKey")
if status in ("CANCELED", "PARTIAL_CANCELED"):
await update_order_status(order_id, status)
await send_refund_email(order_id)
logger.info(f"환불 처리 완료: {order_id} ({status})")
# 반드시 200 반환 (미반환 시 최대 7회 재전송)
return {"ok": True}
웹훅 URL은 토스페이먼츠 개발자센터 > 웹훅 메뉴에서 등록하며, 로컬 개발 환경에서는 ngrok을 사용해 외부 URL을 임시로 생성할 수 있습니다.
웹훅 요청 본문 예시:
{
"eventType": "PAYMENT_STATUS_CHANGED",
"createdAt": "2026-03-02T12:00:00.000",
"data": {
"paymentKey": "B3EvL1cKz9p-kO6XPNpfF",
"status": "CANCELED",
"orderId": "YOWWcpZSDCZ8WJC5x7mkl"
}
}
6. 주요 에러 코드 및 대응 방법
| 에러 코드 | 설명 | 대응 방법 |
|---|---|---|
| ALREADY_PROCESSED_PAYMENT | 이미 처리된 결제 | 중복 승인 방지 로직 추가 |
| PAYMENT_TOKEN_EXPIRED | 결제 토큰 만료 | 결제 재시도 유도 |
| EXCEED_MAX_DAILY_PAYMENT_COUNT | 일일 한도 초과 | 고객 안내 메시지 출력 |
| INVALID_REFUND_ACCOUNT_INFO | 환불 계좌 정보 오류 | 계좌 재입력 요청 |
| EXCEED_MAX_REFUND_DUE | 환불 기한 초과 | 고객센터 연결 안내 |
7. 보안 체크리스트
- 시크릿 키는 서버 환경변수로만 관리하고 절대 클라이언트에 노출하지 마세요.
- 웹훅 엔드포인트는 토스페이먼츠 IP 대역에서의 요청만 허용하도록 방화벽 설정을 검토하세요.
- 결제 승인 전 amount 값을 서버에서 반드시 재검증하세요 (클라이언트 조작 방지).
- 멱등성(Idempotency) 키를 활용해 중복 환불 요청을 방지하세요.
- HTTPS 통신만 허용하고, 웹훅 URL은 외부에 노출되지 않도록 관리하세요.
마무리
토스페이먼츠는 공식 문서가 잘 정리되어 있고 테스트 환경이 충실해 국내 결제 연동 중 진입 장벽이 낮은 편입니다. SDK v2와 웹훅을 조합하면 결제부터 환불까지 완전 자동화된 시스템을 구축할 수 있습니다.
코드벤터는 스타트업과 1인 개발자가 핵심 비즈니스에 집중할 수 있도록 결제 연동, API 설계, 인프라 구성 등 기술적인 부분을 함께 해결해 나가고 있습니다. 토스페이먼츠 연동에 어려움이 있거나 결제 시스템 설계가 필요하다면 언제든지 코드픽(codepick.kr)을 통해 검증된 개발 전문가와 연결해보세요.