# Lucy 모의결제 API 사용 안내 기본 주소: **https://dev.lucypayments.com** 이 환경은 실제 카드 승인·송금·정산이 발생하지 않는 모의 API입니다. 회사별로 발급받은 `apiKey`, `mid`, `inquirySecret`을 사용합니다. 운영 키와 실제 카드·개인정보를 입력하지 마세요. 테스트 키는 서버에서만 보관하고 브라우저 코드에 넣지 않습니다. ## 지원하는 흐름 `회사 키 → TOKEN 발급 → 모의 카드 승인 → 거래 조회 → 부분 취소 → 전체 취소` | 메서드 / 경로 | 인증 | 역할 | |---|---|---| | `POST /api/pay/ready` | Bearer API Key | 해당 MID용 TOKEN 발급, 600초 유효 | | `POST /api/pay/card` | Bearer + TOKEN + Idempotency-Key | 지정된 테스트 카드의 모의 승인·거절 | | `POST /api/pay/transaction` | Bearer + 본문 HMAC 서명 | 회사·MID에 속한 거래 조회 | | `POST /api/pay/cancel` | Bearer + TOKEN + Idempotency-Key | 남은 금액 이내 전체·부분 취소 | | `GET /health` | 없음 | `mode: mock`, 저장소 연결 상태 | 요청은 JSON이며 성공 시 HTTP 200, `resultCode: "0000"`을 반환합니다. 응답의 `sandbox: true`와 `X-Lucy-Environment: sandbox` 헤더로 환경을 확인할 수 있습니다. TOKEN이 만료되면 새로 발급하고 같은 요청·Idempotency-Key로 재시도합니다. ## 테스트 카드와 제한 | 카드번호 | 결과 | |---|---| | `4111111111111111` | 모의 승인 | | `4000000000000002` | 모의 승인 거절: HTTP 200, `resultCode: "9994"` | 만료일은 미래의 `YYYYMM`(예: `209912`), 금액은 1~999,999,999원의 정수 문자열입니다. `currency`는 생략 또는 `KRW`, `quota`는 생략 또는 `00`/`0`, `midChannelId`는 생략 또는 모의 채널 `1`, `taxFreeCd`는 생략 또는 `00`, `taxFreeAmount`는 생략 또는 `0`입니다. 상품명은 최대 100자, 구매자명 64자, 예약메시지 200자입니다. 지원하지 않는 필드·테스트 카드 외 번호는 거절합니다. 회사별 분당 120회, IP별 초당 10회(순간 20회 추가)로 제한합니다. 거래·주문번호·멱등성 기록은 승인 후 7일간 보관하며, 취소해도 보관 기한이 연장되지 않습니다. 보관 기한 이후에는 새 주문번호로 테스트하세요. 서버 재시작 시 데이터는 유지되지만 Redis AOF의 초 단위 기록 특성상 비정상 종료 직전 약 1초의 데이터는 유실될 수 있습니다. 승인·취소의 `Idempotency-Key`는 이 샌드박스에서 **필수**입니다(실 API에서는 선택 사항). 같은 회사·MID·작업·키·본문으로 재요청하면 최초 응답을 반환합니다. 키를 재사용하면서 본문을 바꾸면 `9302`, 이미 사용한 주문번호로 다른 승인 키를 보내면 `9005`입니다. 거절된 주문번호도 재사용하지 않습니다. 별도 부분 취소는 별도 키를 사용하세요. ## 한 번에 실행하는 Node.js 예제 발급받은 비공개 JSON 파일 경로를 인자로 넘깁니다. 비공개 파일에는 `baseUrl`, `mid`, `apiKey`, `inquirySecret`이 있습니다. 예제를 `sandbox-example.cjs`로 저장한 뒤 `node sandbox-example.cjs /private/company.json`으로 실행합니다. 키를 명령줄에 직접 넣지 않습니다. Node.js 22 이상이 필요합니다. ```javascript const fs = require('node:fs'); const { createHmac, randomUUID } = require('node:crypto'); const c = JSON.parse(fs.readFileSync(process.argv[2], 'utf8')); async function post(path, body, headers = {}) { const r = await fetch(c.baseUrl + path, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${c.apiKey}`, ...headers, }, body: JSON.stringify(body), }); const value = await r.json(); if (!r.ok || value.resultCode !== '0000') { throw new Error(`${r.status} ${value.resultCode}: ${value.resultMsg}`); } return value; } async function main() { const { token } = await post('/api/pay/ready', { mid: c.mid }); const orderNo = `test-${randomUUID()}`; const approval = await post('/api/pay/card', { mid: c.mid, orderNo, amount: '1000', productName: '테스트 상품', userName: '테스트 구매자', cardNo: '4111111111111111', expireDt: '209912', quota: '00', reservedIndex: 'company-test', }, { TOKEN: token, 'Idempotency-Key': orderNo }); async function inquiry() { const timestamp = Date.now().toString(); const signature = createHmac('sha256', c.inquirySecret) .update(`${c.mid}|${approval.tid}|${timestamp}`, 'utf8').digest('hex'); const r = await post('/api/pay/transaction', { mid: c.mid, tid: approval.tid, timestamp, signature, }); console.log(r.transaction); } await inquiry(); // APPROVED, left_amount: 1000 for (const [label, amount] of [['partial', '300'], ['final', '700']]) { await post('/api/pay/cancel', { mid: c.mid, tid: approval.tid, amount, cancelReason: '샌드박스 테스트', }, { TOKEN: token, 'Idempotency-Key': `${orderNo}-${label}` }); await inquiry(); // PARTIAL 700 → CANCELED 0 } } main().catch((e) => { console.error(e.message); process.exitCode = 1; }); ``` 조회 서명은 `HMAC-SHA256(inquirySecret, "mid|tid|timestamp")`의 소문자 hex입니다. timestamp는 13자리 Unix 밀리초이며 서버 시간과 ±5분 이내여야 합니다. 조회는 TOKEN을 요구하지 않습니다. 결제 응답의 `settDate`는 모의 승인일이며 정산 실행을 뜻하지 않습니다. ## 주요 결과 코드 | 코드 | 의미 / 대응 | |---|---| | `0000` | 성공 | | `9990` / `9991` | TOKEN 만료·불일치 / API Key 오류(HTTP 401) | | `9003` | MID 또는 조회 서명 권한 오류(403) | | `9997` | 필수값·타입·테스트 카드·멱등성 키 검증 실패(400) | | `9005` / `9302` | 중복 주문 / 같은 멱등성 키의 다른 요청(409) | | `9009` | 해당 회사·MID의 거래 없음 또는 보관 기한 경과(404) | | `9201` / `9202` | 취소 잔액 초과 / 이미 전액 취소(409) | | `9994` | 지정된 거절 테스트 카드의 모의 승인 실패(200) | | `9998` | 회사 요청 한도 초과(429), 잠시 후 재시도 | | `9999` | 저장소·설정 문제(503), 응답 복구 후 같은 키로 재시도 | Nginx에서 거절한 요청(크기 초과·IP 제한·없는 경로)은 JSON 대신 HTTP 오류 페이지일 수 있습니다. 클라이언트는 HTTP 상태와 본문의 resultCode를 함께 처리해야 합니다. ## 이번 환경의 범위 실 PG별 전문·오류·응답 지연, IP 화이트리스트, 체크아웃, 빌링, 가상계좌, 웹훅 재전송, 정산·송금, 운영 DB 반영, 전산 화면은 제공하지 않습니다. `/internal/*` 및 미지원 결제 경로는 등록하지 않았습니다. API의 대표 요청·응답 구조와 거래 생명주기를 테스트하는 환경이며 운영의 모든 정책을 재현하지 않습니다. 운영 전환 시 실제 PG 테스트·가맹점 권한·보안 정책 검증은 별도로 진행합니다. 외부 회사에는 회사별 키 파일을 안전한 채널로 개별 전달합니다. 담당자는 기존 키를 비활성화하거나 새 회사 식별자로 재발급할 수 있습니다. 키 발급·비활성화는 서버 관리자 명령으로만 제공됩니다.