QR·앱 결제 게이트웨이 — 매장(쇼핑몰) 서버 개발자용 연동 문서
외부 공개 문서 · 내부 API(Swagger)와 별개NestPay PG 는 QR/앱 기반 결제 게이트웨이입니다(위챗페이 방식). 매장 서버가 결제 대상(상품·금액·부가세)을 등록하면 NestPay 가 결제용 QR·딥링크를 돌려주고, 사용자가 NestPay 앱으로 결제하면 웹훅과 조회 API로 결과를 알려 줍니다. 유효시간이 지나면 결제 실패로 처리됩니다.
POST /pg/payments (주문 생성) → paymentId·qrData·deeplink·만료시각 수신GET /pg/payments/{id}로도 확인https://pg.nestpay.co.kr · 모든 경로는 /pg 로 시작합니다(예: https://pg.nestpay.co.kr/pg/payments). PG 연동은 이 전용 호스트만 사용합니다(내부 API 와 분리). 유효시간은 NestPay 관리자 설정값을 따릅니다(기본 5분).연동은 4단계로 진행되며, 샌드박스에서 3종 테스트(주문 생성·조회·웹훅 수신)를 모두 통과해야 라이브로 전환됩니다.
| 단계 | 내용 |
|---|---|
| ① 신청 | 매장앱에서 PG 연동 신청 → client_id·api_key(1회 표시) 발급, 웹훅 주소 등록(서명키 1회 표시), 서버 IP 화이트리스트 신청 |
| ② 승인 | NestPay 관리자가 신청·화이트IP 승인 → 샌드박스 모드로 API 사용 가능 |
| ③ 테스트 | 샌드박스에서 주문 생성·조회·웹훅 수신을 각각 1회 이상 성공(자동 기록) |
| ④ 라이브 | 3종 테스트 통과 확인 후 관리자가 라이브 전환 → 실제 결제 시작 |
sandbox 값으로 현재 모드를 확인하세요. 샌드박스와 라이브는 같은 엔드포인트를 쓰며, 승인된 모드에 따라 서버가 구분합니다.모든 /pg 요청에는 아래 4개 헤더가 필요합니다. 승인된 IP 에서만 호출할 수 있습니다.
| 헤더 | 설명 |
|---|---|
X-Client-Id | 발급받은 매장코드 |
X-Api-Key | 발급받은 비밀키 |
X-Timestamp | 요청 시각(Unix epoch 초). 서버 시각과 오차가 크면 거부(재전송 공격 방지) |
X-Signature | 아래 규칙의 HMAC-SHA256 서명(소문자 16진수) |
서명 대상 문자열(canonical)은 다음 4줄을 \n(줄바꿈)으로 이어 붙입니다. 경로는 쿼리스트링을 제외하고,
본문이 없으면 빈 문자열입니다.
canonical = HTTP메서드 + "\n" + 경로(쿼리 제외) + "\n" + X-Timestamp + "\n" + 본문(raw)
signature = HMAC_SHA256(api_key, canonical) 를 소문자 16진수로
# 예: 주문 생성 요청 서명 (Python)
import hmac, hashlib, time, json, requests
CLIENT_ID = "mc_xxxxxxxxxxxx"; API_KEY = "발급받은_비밀키"
body = json.dumps({"orderNo":"SHOP-1001","itemName":"주문 #1001","amount":11000,"vatIncluded":True})
ts = str(int(time.time()))
canonical = f"POST\n/pg/payments\n{ts}\n{body}"
sig = hmac.new(API_KEY.encode(), canonical.encode(), hashlib.sha256).hexdigest()
r = requests.post("https://pg.nestpay.co.kr/pg/payments", data=body, headers={
"Content-Type": "application/json",
"X-Client-Id": CLIENT_ID, "X-Api-Key": API_KEY, "X-Timestamp": ts, "X-Signature": sig,
})
공통 응답 포장: { "success": true, "data": { ... }, "error": null }. 실패 시 success:false, error에 사유.
/pg/payments — 결제 주문 생성| 요청 필드 | 필수 | 설명 |
|---|---|---|
orderNo | Y | 매장 주문번호(매장별 유일). 같은 값 재요청 시 기존 주문을 그대로 반환(멱등) |
itemName | Y | 결제 대상 이름 |
amount | Y | 청구 총액(원, 0 초과) |
vatIncluded | Y | true=금액에 부가세 포함(서버가 공급가/부가세 10% 분리) · false=면세(부가세 0) |
// 응답 data
{
"paymentId": 1024, "orderNo": "SHOP-1001", "status": "PENDING",
"amount": 11000, "supplyAmount": 10000, "vatAmount": 1000, "itemName": "주문 #1001",
"qrData": "NPQR1.xxxx", // QR 로 그려 사용자에게 표시
"deeplink": "nestpay://pay?token=NPQR1.xxxx", // 모바일 웹은 이 링크로 앱 열기
"expiresAt": "2026-07-26T12:34:56", "expiresInSeconds": 300, "sandbox": true
}
/pg/payments/{paymentId} — 결제 조회웹훅을 못 받았거나 확인이 필요할 때 언제든 폴링할 수 있습니다. status로 결과를 판단하세요.
// 응답 data
{ "paymentId":1024, "orderNo":"SHOP-1001", "status":"PAID", "amount":11000,
"txnId":55231, "paidAt":"2026-07-26T12:31:10", "sandbox":true }
/pg/payments/{paymentId}/cancel — 취소 / 환불하나의 엔드포인트가 상태에 따라 동작합니다. 부분취소·부분환불은 제공하지 않습니다(전액만).
| 주문 상태 | 동작 |
|---|---|
| PENDING(미결제) | 취소 — QR 소각(이후 결제 시도 불가) + 상태 CANCELED + 취소 웹훅 |
| PAID(결제완료) | 전액 환불 — 사용자에게 전액 환불(원장 역분개) + 상태 CANCELED + 환불 웹훅 |
| EXPIRED | 취소 불가(이미 만료). 별도 조치 불필요 |
| CANCELED | 멱등 — 그대로 CANCELED 반환 |
/pg/payments?status=&page=&limit= — 결제 내역매장 본인 결제 내역(상태 필터·페이징). 응답: { items:[…], total, page, limit }.
/pg/balance — 정산 예정 잔액{ "settlementBalance": 452000, "paidCount": 61, "paidSum": 690000, "refundedCount": 3 }
결제 결과가 확정되면 등록한 주소로 POST합니다. 본문은 JSON, 헤더로 이벤트와 서명이 옵니다.
| 헤더 | 설명 |
|---|---|
X-Nestpay-Event | 이벤트 종류(아래) |
X-Nestpay-Signature | HMAC_SHA256(webhook_secret, 본문) 소문자 16진수 — 반드시 검증 |
| 이벤트 | 의미 |
|---|---|
PAYMENT_COMPLETED | 결제 완료(status=PAID) |
PAYMENT_EXPIRED | 유효시간 경과로 결제 실패(status=EXPIRED) |
PAYMENT_CANCELED | 미결제 주문 취소(status=CANCELED) |
PAYMENT_REFUNDED | 결제완료건 전액 환불(status=REFUNDED) |
// 웹훅 본문 예
{ "event":"PAYMENT_COMPLETED", "paymentId":1024, "orderNo":"SHOP-1001", "amount":11000, "status":"PAID", "txnId":55231 }
// 서명 검증(Python)
import hmac, hashlib
def valid(body_bytes, header_sig, webhook_secret):
calc = hmac.new(webhook_secret.encode(), body_bytes, hashlib.sha256).hexdigest()
return hmac.compare_digest(calc, header_sig)
200을 돌려주고 처리는 비동기로 하세요.paymentId+event 기준으로
이미 처리한 건이면 무시하도록 구현하세요.| 상황 | NestPay 동작 | 매장(쇼핑몰) 처리 |
|---|---|---|
| 정상 결제 | 결제요청 → 결제 → PAYMENT_COMPLETED 웹훅/조회 PAID | 주문 결제완료 처리 |
| 결제 후, 이미 쇼핑몰 주문이 취소됨 | 환불요청(/cancel) → PAYMENT_REFUNDED | 환불요청 호출 후 환불 확정 처리 |
| 미결제로 유효시간 경과 | 자동 만료 → PAYMENT_EXPIRED, 조회 EXPIRED | 주문 미결제 처리(장바구니 복원 등) |
| 미결제 상태에서 쇼핑몰이 주문 취소 | 취소요청(/cancel) → QR 소각 → PAYMENT_CANCELED | 취소 확정 처리 |
| 취소 후 사용자가 뒤늦게 결제 시도 | QR 이 소각돼 결제 거부(에러) | 추가 처리 불필요(이미 취소됨) |
| 웹훅 유실(재시도 소진) | 발송 중단 | 조회 API 폴링으로 최종 상태 확정(웹훅과 조회는 상호 보완) |
| 웹훅 중복 수신 | 재시도로 같은 이벤트 재발송 가능 | 멱등 처리(paymentId+event 기준 1회만) |
| 주문 생성 재요청(동일 orderNo) | 기존 주문 그대로 반환(중복 생성 방지) | 동일 결제창 재사용 |
| 만료와 결제가 거의 동시 | QR·주문 만료 시각이 동일 → 만료 후 결제는 거부. 결제가 먼저면 PAID 고정 | 조회 API 최종 상태를 신뢰 |
주문 상태(status): PENDING(대기) · PAID(완료) · EXPIRED(만료) · CANCELED(취소/환불).
| HTTP | 의미 / 대응 |
|---|---|
| 200 | 정상. 단 success가 false면 error 확인 |
| 400 | 필수값 누락·형식 오류·취소 불가 상태 등 요청 문제 |
| 401 | 서명 불일치·헤더 누락·시각 오차(HMAC 재확인) |
| 403 | 미승인 화이트IP·본인 매장 아님 |
| 404 | 주문 없음 |
| 429 | 호출 과다(이용제한) — 잠시 후 재시도 |