온라인 쇼핑몰이나 웹 서비스를 구축할 때 카카오페이 간편결제 연동 요청을 자주 받게 됩니다. 하지만 API 문서를 보며 결제 준비(Ready)와 승인(Approve) 단계를 연동하다가 HTTP 400 에러나 파라미터 불일치로 결제 승인이 실패하는 상황을 흔히 경험해봤을 겁니다.
다만 대부분의 개발자들은 카카오페이가 요구하는 Secret Key 인증 방식과 redirect_url 처리 흐름을 정확히 이해하지 못한 채 인터넷에서 가져온 예제 코드를 그냥 복사해 붙이다가 오류를 겪곤 합니다.
이번에는 카카오페이 단기 결제 API의 정확한 동작 원리가 무엇인지, 왜 결제가 준비와 승인 2단계로 나뉘는지, 그리고 PHP cURL을 이용해 실무에서 안전하게 구현하는 방법을 완벽하게 정리해서 소개하겠습니다.

 

1. 카카오페이 단기 결제 API의 동작 원리

카카오페이 단기 결제 API는 보안 강화를 위해 클라이언트가 직접 결제 승인을 요청하지 않고, 서버 간 통신(Server-to-Server)으로 결제를 검증하는 2단계 방식을 취합니다.
사용자가 '결제하기' 버튼을 누르면 서버에서 카카오페이로 결제 준비(Ready) 요청을 보내고, 전달받은 next_redirect_pc_url로 사용자를 이동시킵니다. 사용자가 카카오톡 앱에서 결제를 승인하면, 사전 설정한 approval_url로 카카오페이가 pg_token을 전달하며, 이를 이용해 서버에서 최종 결제 승인(Approve) API를 호출하는 구조입니다.

이 과정의 핵심 개념을 비교하면 다음과 같습니다.

구분결제 준비 (Ready API)결제 승인 (Approve API)
주요 역할결제 요청 정보를 등록하고 결제 URL 및 TID 발급사용자 인증 완료 후 전달받은 pg_token으로 실제 결제 확정
필수 파라미터cid, partner_order_id, partner_user_id, item_name, quantity, total_amount 등cid, tid, partner_order_id, partner_user_id, pg_token
응답 데이터tid (거래 번호), next_redirect_pc_url (결제 페이지 URL)aid, tid, payment_method_type, amount (결제 상세 정보)

 

2. 카카오페이 API 연동 사전 준비

카카오페이 API를 연동하기 위해서는 카카오 개발자 센터(Kakao Developers)에서 애플리케이션을 생성하고 결제 전용 키를 발급받아야 합니다.
과거에는 Admin Key 방식을 사용했으나, 보안 정책 업데이트로 현재는 Secret Key(가맹점 전용 비밀키) 기반 인증 헤더를 사용해야 합니다.

요청 헤더 구성 시 다음과 같이 작성합니다.

  • Authorization: SECRET_KEY ${DEV_SECRET_KEY} (테스트 환경인 경우 발급받은 DEV_로 시작하는 개발용 Secret Key 사용)
  • Content-Type: application/json;charset=UTF-8

 

3. 실전 예제: PHP cURL로 결제 연동하기

이제 PHP에서 결제 준비(Ready) 단계부터 결제 승인(Approve) 단계까지의 코드를 직접 구현해 보겠습니다.

 

1단계: 결제 준비(Ready) 요청 구현

결제 준비 요청 시에는 상품 정보와 결제 성공/실패/취소 시 이동할 URL을 전달해야 합니다. 응답으로 받은 tid(거래 고유번호)는 승인 단계에서 반드시 필요하므로 세션(Session)이나 DB에 반드시 저장해야 합니다.

✗ 잘못된 코드 (자주 하는 실수: 구버전 인증 헤더 사용 및 TID 세션 저장 누락)

<?php
// 잘못된 예시: 구버전 Admin Key 사용 및 TID를 저장하지 않음
$ch = curl_init('https://open-api.kakaopay.com/online/v1/payment/ready');
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: KakaoAK YOUR_ADMIN_KEY', // 구버전 방식 사용 시 401/400 에러 발생
    'Content-Type: application/x-www-form-urlencoded'
]);
// JSON 형태가 아닌 쿼리 스트링 전송 실수
curl_setopt($ch, CURLOPT_POSTFIELDS, "cid=TC0ONETIME&partner_order_id=1001...");
$response = curl_exec($ch);
curl_close($ch);

// tid를 저장하지 않고 단순 URL 리다이렉트만 진행함 (승인 단계에서 TID 분실)
$data = json_decode($response, true);
header('Location: ' . $data['next_redirect_pc_url']);
?>

✓ 올바른 코드 (Secret Key 사용, JSON 바디 전송 및 TID 세션 저장)

<?php
session_start();

$secretKey = 'DEV_SECRET_KEY_YOUR_KEY_HERE'; // 카카오페이에서 발급받은 Secret Key
$url = 'https://open-api.kakaopay.com/online/v1/payment/ready';

$orderId = 'ORDER_' . time();
$userId = 'USER_12345';

$params = [
    'cid' => 'TC0ONETIME', // 가맹점 코드 (테스트용: TC0ONETIME)
    'partner_order_id' => $orderId,
    'partner_user_id' => $userId,
    'item_name' => '테스트 상품',
    'quantity' => 1,
    'total_amount' => 10000,
    'tax_free_amount' => 0,
    'approval_url' => 'https://yourdomain.com/kakao_approve.php',
    'cancel_url' => 'https://yourdomain.com/kakao_cancel.php',
    'fail_url' => 'https://yourdomain.com/kakao_fail.php'
];

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: SECRET_KEY ' . $secretKey,
    'Content-Type: application/json;charset=UTF-8'
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($params));

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

if ($httpCode === 200) {
    $result = json_decode($response, true);
    
    // 핵심: 승인 단계를 위해 tid와 주문 정보를 세션에 안전하게 보관
    $_SESSION['kakao_tid'] = $result['tid'];
    $_SESSION['partner_order_id'] = $orderId;
    $_SESSION['partner_user_id'] = $userId;
    
    // PC 결제 페이지로 리다이렉트
    header('Location: ' . $result['next_redirect_pc_url']);
    exit;
} else {
    echo "결제 준비 실패 (HTTP " . $httpCode . "): " . htmlspecialchars($response);
}
?>

결제 준비 성공 시 카카오페이로부터 다음과 같은 JSON 응답을 받게 됩니다.

{
  "tid": "T1234567890123456789",
  "next_redirect_pc_url": "https://online-pay.kakaopay.com/mockup/v1/123456789...",
  "created_at": "2023-10-25T12:00:00"
}

 

2단계: 결제 승인(Approve) 요청 구현

사용자가 결제를 완료하면 카카오페이는 approval_url로 GET 파라미터 pg_token을 전달하며 페이지를 이동시킵니다. 세션에 저장해 둔 tid와 수신된 pg_token을 합쳐 최종 승인 API를 호출합니다.

✓ 올바른 코드 (kakao_approve.php)

<?php
session_start();

$pgToken = $_GET['pg_token'] ?? '';
$tid = $_SESSION['kakao_tid'] ?? '';
$orderId = $_SESSION['partner_order_id'] ?? '';
$userId = $_SESSION['partner_user_id'] ?? '';

if (empty($pgToken) || empty($tid)) {
    die('잘못된 접근이거나 세션 정보가 만료되었습니다.');
}

$secretKey = 'DEV_SECRET_KEY_YOUR_KEY_HERE';
$url = 'https://open-api.kakaopay.com/online/v1/payment/approve';

$params = [
    'cid' => 'TC0ONETIME',
    'tid' => $tid,
    'partner_order_id' => $orderId,
    'partner_user_id' => $userId,
    'pg_token' => $pgToken
];

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: SECRET_KEY ' . $secretKey,
    'Content-Type: application/json;charset=UTF-8'
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($params));

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

if ($httpCode === 200) {
    $result = json_decode($response, true);
    // 세션 임시 데이터 삭제
    unset($_SESSION['kakao_tid'], $_SESSION['partner_order_id'], $_SESSION['partner_user_id']);
    
    echo "결제가 성공적으로 완료되었습니다! 주문번호: " . htmlspecialchars($result['partner_order_id']);
} else {
    echo "결제 승인 실패 (HTTP " . $httpCode . "): " . htmlspecialchars($response);
}
?>

결제 승인 성공 시 결과 출력값 예시:

{
  "aid": "A1234567890123456789",
  "tid": "T1234567890123456789",
  "cid": "TC0ONETIME",
  "partner_order_id": "ORDER_1698200000",
  "partner_user_id": "USER_12345",
  "payment_method_type": "MONEY",
  "amount": {
    "total": 10000,
    "tax_free": 0,
    "vat": 909
  },
  "approved_at": "2023-10-25T12:01:30"
}

 

4. 흔히 하는 실수 및 주의사항

실무 연동 도중 가장 자주 발생하는 문제들과 올바른 해결 방안은 다음과 같습니다.

  • 파라미터 불일치 에러 (-702 에러):
    ✗ 결제 준비(Ready) 때 전달한 partner_order_id, partner_user_id와 결제 승인(Approve) 시 전달하는 값이 다르면 카카오페이에서 결제를 거부합니다.
    ✓ 준비 요청 당시의 주문/회원 정보 ID를 세션이나 데이터베이스에 엄격히 보관하고 그대로 승인 요청에 사용해야 합니다.
  • 세션 유실로 인한 TID 상실:
    ✗ 사파리(Safari) 브라우저나 도메인 간 리다이렉트 과정에서 Cookie SameSite 정책으로 인해 세션이 끊기는 경우가 발생합니다.
    ✓ 도메인 세션 쿠키의 SameSite 속성을 Lax 또는 None; Secure로 적절히 설정하거나, DB 테이블에 주문번호 기준으로 TID를 기록하는 방식을 권장합니다.
  • 이중 결제 승인 요청:
    ✗ 사용자가 새로고침을 누르거나 네트워크 지연으로 승인 요청이 2번 들어가는 경우 에러가 발생합니다.
    ✓ 승인 요청 전 DB에서 이미 완료된 주문인지 상태값 검증을 거치는 로직이 필수적입니다.

 

5. 마무리

카카오페이 API 연동은 결제 트랜잭션의 안전한 흐름 관리가 핵심입니다. TID 세션 관리와 정확한 인증 헤더 설정이라는 작은 최적화와 습관이 모여서 안정적인 결제 시스템을 만든다는 점을 잊지 말자. 이 글의 결제 준비 및 승인 코드 구현 부분을 참고해 보안 검증을 추가한 결제 모듈을 작성하면, 오류 없는 깔끔한 간편결제 시스템을 구축할 수 있을 것입니다.