토스페이먼츠(Toss Payments) 결제창 연동 중 프론트엔드에서 결제 인증을 마친 뒤 서버에서 최종 승인(Confirm) 요청을 보낼 때 401 Unauthorized 에러나 승인 실패를 경험해본 적이 있을 것이다. 대부분의 경우 API 시크릿 키를 HTTP Basic 인증 헤더로 변환하는 과정에서 포맷을 잘못 맞추었거나, 승인 요청 시 필요한 파라미터 전달 형식이 틀렸기 때문이다.

웹 서비스에서 결제 시스템은 매출과 직결되는 가장 중요한 파이프라인이다. 이번에는 토스페이먼츠 REST API 결제 승인 메커니즘이 정확히 어떻게 동작하는지, 왜 시크릿 키 인증 에러가 발생하는지, 그리고 PHP cURL을 이용해 안정적으로 결제 승인(Confirm)을 처리하는 방법까지 완벽하게 정리해서 소개하겠다.

 

토스페이먼츠 결제 승인 메커니즘 이해하기

토스페이먼츠의 결제 흐름은 보안을 위해 2단계로 분리되어 있다. 웹 브라우저(클라이언트)에서 결제창을 통해 카드 정보 입력 및 본인 인증을 마치면, 토스페이먼츠 서버는 사용자를 개발자가 지정한 successUrl로 리다이렉트시킨다. 이때 URL 쿼리 파라미터로 paymentKey, orderId, amount 세 가지 핵심 정보가 전달된다.

하지만 이 단계는 '인증 완료'일 뿐, 실제 돈이 출금되거나 카드 승인이 일어난 상태가 아니다. 백엔드 서버에서 이 3가지 파라미터를 받아 토스페이먼츠 승인 API(https://api.tosspayments.com/v1/payments/confirm)로 서버 대 서버 통신을 요청해야 비로소 최종 결제가 완료된다.

 

토스페이먼츠 인증 헤더 구조

토스페이먼츠 REST API는 HTTP Basic 인증 방식을 사용한다. API 시크릿 키 뒤에 콜론(:)을 붙인 상태에서 Base64로 인코딩한 값을 Authorization 헤더에 넘겨주어야 한다. 많은 개발자가 콜론을 누락하거나 일반 Bearer 토큰 방식으로 헤더를 전송하여 401 에러를 발생시킨다.

 

구분헤더 이름전송 값 포맷주의사항
Basic 인증AuthorizationBasic {Base64(시크릿키 + :)}시크릿 키 뒤에 반드시 콜론(:)이 포함되어야 함
요청 데이터 포맷Content-Typeapplication/jsonPOST 요청 바디는 JSON 문자열이어야 함
중복 요청 방지Idempotency-Key유니크한 문자열 (선택)네트워크 재시도 시 중복 결제 승인 방지

 

PHP cURL 기반 결제 승인 연동 실전 예제

이제 프론트엔드에서 넘어온 paymentKey, orderId, amount 데이터를 받아 토스페이먼츠 서버로 결제 승인을 요청하는 코드 구현을 살펴보겠다.

 

✗ 잘못된 코드 예시

다음은 시크릿 키 뒤에 콜론을 누락하고, Authorization 헤더 포맷을 잘못 설정하여 승인이 거부되는 전형적인 오류 코드다.

 

<?php
$secretKey = "test_sk_zXLk5nO28q6m712345678";
$paymentKey = $_GET['paymentKey'];
$orderId = $_GET['orderId'];
$amount = $_GET['amount'];

// ✗ 잘못된 방식: 시크릿 키 뒤에 콜론(:) 없이 base64 인코딩 진행
$credential = base64_encode($secretKey);

$ch = curl_init('https://api.tosspayments.com/v1/payments/confirm');
curl_setopt($ch, CURLOPT_POST, true);
// ✗ 잘못된 방식: Basic 대신 Bearer를 사용하거나 헤더 배열 포맷 오류
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer ' . $credential,
    'Content-Type: application/x-www-form-urlencoded'
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
    'paymentKey' => $paymentKey,
    'orderId' => $orderId,
    'amount' => $amount
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);
echo $response;
?>

 

위 코드를 실행하면 토스페이먼츠 API 서버는 401 Unauthorized 또는 400 Bad Request 응답을 반환하며 결제 승인을 거부한다. HTTP Basic 인증 규격을 지키지 않았고, 요청 바디를 JSON이 아닌 Form-Urlencoded 형식으로 보냈기 때문이다.

 

✓ 올바른 코드 예시

시크릿 키 인코딩 시 콜론을 명시하고, JSON 바디 전송 및 데이터베이스에 기록된 주문 금액과 클라이언트 요청 금액을 사전에 검증하는 올바른 코드다.

 

<?php
// 1. 토스페이먼츠 시크릿 키 설정 (시크릿 키 + 콜론)
$widgetSecretKey = "test_sk_zXLk5nO28q6m712345678";
$base64SecretKey = base64_encode($widgetSecretKey . ":");

// 2. 콜백 파라미터 수신
$paymentKey = $_GET['paymentKey'] ?? '';
$orderId = $_GET['orderId'] ?? '';
$amount = (int)($_GET['amount'] ?? 0);

// 3. DB 사전 검증 (가상 로직: 클라이언트가 전달한 금액 위변조 체크)
/*
$originalOrder = getOrderFromDatabase($orderId);
if ($originalOrder['amount'] !== $amount) {
    die(json_encode(['error' => '결제 금액 위변조 감지']));
}
*/

// 4. Toss Payments Confirm API 호출 데이터 준비
$postData = json_encode([
    'paymentKey' => $paymentKey,
    'orderId' => $orderId,
    'amount' => $amount
]);

$ch = curl_init('https://api.tosspayments.com/v1/payments/confirm');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Basic ' . $base64SecretKey,
    'Content-Type: application/json'
]);

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);

if ($curlError) {
    die(json_encode(['status' => 'FAIL', 'message' => 'cURL 통신 오류: ' . $curlError]));
}

$resultData = json_decode($response, true);

if ($httpCode === 200 && isset($resultData['status']) && $resultData['status'] === 'DONE') {
    // 결제 성공: DB 주문 상태 업데이트 및 리다이렉트 처리
    echo json_encode([
        'status' => 'SUCCESS',
        'method' => $resultData['method'],
        'totalAmount' => $resultData['totalAmount'],
        'approvedAt' => $resultData['approvedAt']
    ]);
} else {
    // 결제 승인 실패 처리
    echo json_encode([
        'status' => 'FAIL',
        'code' => $resultData['code'] ?? 'UNKNOWN_ERROR',
        'message' => $resultData['message'] ?? '결제 승인에 실패했습니다.'
    ]);
}
?>

 

✓ 응답 출력 결과 예시 (성공 시)

 

{
  "status": "SUCCESS",
  "method": "카드",
  "totalAmount": 15000,
  "approvedAt": "2024-03-20T14:30:00+09:00"
}

 

실무에서 흔히 놓치는 주의사항

결제 승인 API를 구현할 때 작동만 된다고 끝이 아니다. 실제 운영 환경에서 발생할 수 있는 보안 취약점과 시스템 장애를 방지하기 위해 다음 3가지를 반드시 점검해야 한다.

  • 결제 금액 위변조 검증: 프론트엔드에서 전달되는 amount 파라미터는 사용자가 F12 개발자 도구 등으로 손쉽게 변경할 수 있다. Confirm API를 호출하기 전에 서버 DB에 저장된 실제 주문 금액과 일치하는지 반드시 대조해야 한다.
  • Idempotency-Key(멱등성 키) 사용: 네트워크 불안정으로 인해 cURL 요청 타임아웃이 발생하였을 때 동일한 결제 건을 재시도하면 중복 승인 에러가 발생한다. 헤더에 Idempotency-Key: {orderId}를 포함하여 요청하면 동일한 요청임을 토스 서버가 인식하여 안전하게 응답을 재전송받을 수 있다.
  • 테스트 키와 실서버 키 분리: 개발 환경의 test_sk_ 키와 운영 환경의 live_sk_ 키는 엄격히 분리하여 환경 변수(ENV)로 관리해야 한다. 시크릿 키가 Git 레포지토리에 유출되지 않도록 주의하자.

 

Toss Payments API 연동은 결제 승인 단계에서의 정확한 Basic 인증과 사전 금액 검증이 핵심이다. 개발 과정에서의 철저한 예외 처리와 보안 검증이라는 작은 습관이 모여서 서비스의 높은 신뢰성과 결제 장애 제로라는 큰 효과를 만든다는 점을 잊지 말자. 이 글의 실전 PHP cURL 코드를 참고해 결제 승인 모듈을 구축하면, 오류 없는 안전한 결제 시스템을 구현할 수 있을 것이다.