QR·앱 결제 게이트웨이 — 매장(쇼핑몰) 서버 개발자용 연동 문서
외부 공개 문서 · 내부 API(Swagger)와 별개NestPay PG 는 QR/앱 기반 결제 게이트웨이입니다(위챗페이 방식). 매장 서버가 결제 대상(상품·금액·부가세)을 등록하면 NestPay 가 결제용 QR·딥링크를 돌려주고, 사용자가 NestPay 앱으로 결제하면 웹훅과 조회 API로 결과를 알려 줍니다. 유효시간이 지나면 결제 실패로 처리됩니다.
POST /pg/payments (주문 생성) → paymentId·qrData·deeplink·만료시각 수신GET /pg/payments/{id}로도 확인https://npwp-pg.nestpay.co.kr
← 지금 보고 있는 서버 기준으로 자동 표시됩니다 ·
모든 경로는 /pg 로 시작합니다(예: https://npwp-pg.nestpay.co.kr/pg/payments).
운영은 npwp-pg.nestpay.co.kr, 테스트는 npwp-tpg.nestpay.co.kr 입니다. PG 연동은 이 전용 호스트만 사용합니다(내부 API 와 분리). 유효시간은 NestPay 관리자 설정값을 따릅니다(기본 5분).연동은 4단계로 진행되며, 샌드박스에서 4종 테스트를 모두 통과해야 라이브로 전환됩니다.
| 단계 | 내용 |
|---|---|
| ① 신청 | 매장앱에서 PG 연동 신청 → client_id·api_key(1회 표시) 발급, 웹훅 주소 등록(서명키 1회 표시), 서버 IP 화이트리스트 신청 |
| ② 승인 | NestPay 관리자가 신청·화이트IP 승인 → 샌드박스 모드로 API 사용 가능 |
| ③ 테스트 | 샌드박스에서 아래 4종을 통과(자동 기록). 통과 조건이 각각 다릅니다 — 바로 아래 표 참고 |
| ④ 라이브 | 4종 테스트 통과 확인 후 관리자가 라이브 전환 → 실제 결제 시작 |
| 항목 | 통과 조건 |
|---|---|
| ① 결제 생성 | POST /pg/payments 로 주문을 만들면 통과합니다. |
| ② 결제 조회 | GET /pg/payments/{id} 로 결제가 끝난 주문(PAID 또는 CANCELED)을 읽어야 통과합니다.
대기(PENDING) 상태만 조회하면 통과하지 않습니다 — 결제 결과를 실제로 읽는지 확인하기 위해서입니다. |
| ③ 웹훅 수신 | 등록한 콜백 주소로 PAYMENT_COMPLETED 알림을 받고 2xx 로 답해야 통과합니다.
만료(PAYMENT_EXPIRED) 알림만 받은 경우는 통과하지 않습니다. |
| ④ 서명 검증 | 매장앱의 [서명 검증 시험 실행] 을 누르면, NestPay 가 일부러 서명을 틀리게 만든 시험 알림
(event: "SIGNATURE_TEST")을 콜백 주소로 한 번 보냅니다.
쇼핑몰이 4xx 로 거절하면 통과, 2xx 로 답하면 불통과입니다. |
X-Nestpay-Signature 를 서명키로 검증하고, 다르면 4xx 로 거절하세요.sandbox 값으로 현재 모드를 확인하세요.
샌드박스와 라이브는 같은 주소·같은 열쇠를 쓰며, 승인된 모드에 따라 서버가 구분합니다.모든 /pg 요청에는 아래 4개 헤더가 필요합니다. 승인된 IP 에서만 호출할 수 있습니다.
| 헤더 | 설명 |
|---|---|
X-Client-Id | 발급받은 매장코드 |
X-Api-Key | 발급받은 비밀키 |
X-Timestamp | 요청 시각(Unix epoch 초). 서버 시각과 오차가 크면 거부. 상태를 바꾸는 요청(POST 등)의 서명은 1회용이라, 같은 서명을 다시 보내면 거부됩니다(재전송·중복 처리 방지). 재시도는 시각을 갱신해 새 서명으로 보내세요(조회 GET 은 제외). |
X-Signature | 아래 규칙의 HMAC-SHA256 서명(소문자 16진수) |
서명 대상 문자열(canonical)은 다음 4줄을 \n(줄바꿈)으로 이어 붙입니다. 경로에는 쿼리스트링(?status=…&page=…)까지 포함합니다(GET 의 쿼리도 위·변조 방지를 위해 서명 대상). 본문이 없으면 빈 문자열입니다.
canonical = HTTP메서드 + "\n" + 경로(쿼리 포함) + "\n" + X-Timestamp + "\n" + 본문(raw)
signature = HMAC_SHA256(api_key, canonical) 를 소문자 16진수로
# 예: GET 이면 경로 자리에 "/pg/payments?status=PAID&page=1&limit=20" 처럼 보낸 그대로(쿼리 포함) 씁니다.
# 예: 주문 생성 요청 서명 (Python)
import hmac, hashlib, time, json, requests
CLIENT_ID = "mc_xxxxxxxxxxxx"; API_KEY = "발급받은_비밀키"
body = json.dumps({"orderNo":"SHOP-1001","amount":11000,"vatIncluded":True,
"items":[{"name":"아메리카노","qty":2,"unitPrice":4000},
{"name":"쿠키","qty":1,"unitPrice":3000}], # 수량×단가 합 = amount
"returnInfo":"SHOP-ORDER-1001"}) # 임의 정보 — 그대로 echo
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://npwp-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 | 조건부 | 결제 대상 이름(단일 상품). items를 보내면 생략 가능 — 생략 시 "첫 상품명 외 N건"으로 자동 생성 |
amount | Y | 청구 총액(원, 0 초과) |
vatIncluded | Y | true=금액에 부가세 포함(서버가 공급가/부가세 10% 분리) · false=면세(부가세 0) |
items | N | 다품목 목록(최대 100개) — 상품이 여러 개이거나 상품별 수량이 다른 묶음 결제.
각 원소는 { name(상품명·200자), qty(수량 1~9999), unitPrice(단가·원), taxFree(선택) }.
모든 품목의 수량×단가 합이 amount와 정확히 일치해야 하며, 다르면 400 거절(금액 무결성).taxFree: true=이 품목 면세 · false=과세(품목 금액에 부가세 포함 → 분리) ·
생략 시 주문 전체 vatIncluded를 따름. 과세·면세 혼합 장바구니(예: 면세 1만원 + 과세 2만원)는
품목별로 공급가/부가세를 나눈 뒤 합산합니다(주문의 supplyAmount/vatAmount = 품목 합).
품목 목록은 과세구분·공급가·부가세까지 주문 시점 스냅샷으로 보존되어 조회 때 그대로 돌려받습니다 |
returnInfo | N | 쇼핑몰의 임의 정보(자체 주문코드 등, 500자 이하). NestPay는 내용을 해석하지 않고 생성 응답·결제 조회·모든 웹훅에 그대로 되돌려줍니다(echo) — 쇼핑몰 주문과의 대사(매칭)용 |
// 응답 data
{
"paymentId": 1024, "orderNo": "SHOP-1001", "status": "PENDING",
"amount": 11000, "supplyAmount": 10000, "vatAmount": 1000,
"itemName": "아메리카노 외 1건", // items 만 보냈으면 자동 생성된 대표 이름
"returnInfo": "SHOP-ORDER-1001", // 보낸 리턴정보를 그대로 echo
"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,
"itemName":"아메리카노 외 1건",
"items":[ {"name":"아메리카노","qty":2,"unitPrice":4000,"amount":8000, // 주문 시점 품목 스냅샷
"taxFree":false,"supplyAmount":7273,"vatAmount":727}, // 품목별 과세구분·공급가·부가세
{"name":"쿠키","qty":1,"unitPrice":3000,"amount":3000,
"taxFree":true,"supplyAmount":3000,"vatAmount":0} ], // (다품목이 아니면 null)
"returnInfo":"SHOP-ORDER-1001", // 보낸 값 그대로 echo
"supplyAmount":10273, "vatAmount":727, // 주문 전체 공급가·부가세
"txnId":55231, // 결제 확정 시 생기는 거래번호(미결제면 null)
"paidAt":"2026-07-26T12:31:10", // 결제 완료 시각(미결제면 null)
"canceledAt":null, // 취소·환불 시각(없으면 null)
"sandbox":true }
// 모든 시각은 KST(한국시간) 기준입니다.
/pg/payments/{paymentId}/cancel — 취소 / 환불하나의 엔드포인트가 상태에 따라 동작합니다. 부분취소·부분환불은 제공하지 않습니다(전액만).
| 주문 상태 | 동작 |
|---|---|
| PENDING(미결제) | 취소 — QR 소각(이후 결제 시도 불가) + 상태 CANCELED + 취소 웹훅 |
| PAID(결제완료) | 전액 환불 — 사용자에게 전액 환불(원장 역분개) + 상태 CANCELED + 환불 웹훅 |
| EXPIRED | 취소 불가(이미 만료). 별도 조치 불필요 |
| CANCELED | 멱등 — 그대로 CANCELED 반환 |
{ "paymentId": 22, "status": "CANCELED", "refunded": false }
// 결제완료 건을 환불한 경우 → { "status":"CANCELED", "refunded":true, "refundAmount":11000 }
// refunded=false → 미결제 주문을 취소한 것 · true → 결제완료 건을 전액 환불한 것
status 라는 이름이 두 곳에서 다른 값을 가집니다.CANCELED 입니다(조회 API·취소 응답 모두). 별도의 REFUNDED 주문 상태는 없습니다.status 는 "REFUNDED" 로 옵니다(그 알림이 환불이었음을 알리는 라벨입니다).status 를 그대로 주문 상태로 저장하면, 나중에 조회 API 와 값이 어긋납니다.
주문 상태는 조회 API 의 status 를 기준으로 삼고, 환불 여부는 event(PAYMENT_REFUNDED)로 판단하세요./pg/payments?status=&page=&limit= — 결제 내역매장 본인 결제 내역만 돌려줍니다(다른 매장 건은 보이지 않습니다). status 는 생략하면 전체입니다.
{ "total": 14, "page": 1, "limit": 1,
"items": [
{ "paymentId": 22, "orderNo": "SAMPLE-1001", "status": "PENDING",
"amount": 11000, "supplyAmount": 11000, "vatAmount": 0,
"itemName": "아메리카노 외 1건", "returnInfo": "SHOP-ORDER-1001",
"createdAt": "2026-08-12T04:30:04", // 주문 만든 시각
"paidAt": null, // 결제 완료 시각(미결제면 null)
"canceledAt": null, // 취소·환불 시각(없으면 null)
"txnId": null, // 결제 확정 시 생기는 거래번호
"sandbox": false }
] }
// 목록 항목에는 items(품목 목록)가 들어가지 않습니다 — 품목은 단건 조회로 확인하세요.
// 모든 시각은 KST(한국시간) 기준입니다.
/pg/balance — 정산 예정 잔액{ "settlementBalance": 452000, "paidCount": 61, "paidSum": 690000, "refundedCount": 3 }
/pg/ping — 연동(HMAC) 연결 시험발급 정보·서명·화이트IP 설정이 올바른지 확인하는 무해한 시험 호출입니다(상태 변경 없음). 연동을 처음 붙일 때 이것부터 200 이 되게 만드세요 — 서명·IP 문제를 먼저 걸러 냅니다.
{ "status": "OK", "merchantId": 1, "mode": "LIVE" }
// mode : "SANDBOX"(시험) 또는 "LIVE"(실거래). 지금 어느 모드인지 여기서 확인합니다.
// 본문은 비워도 됩니다(빈 객체 {} 를 보내세요 — 서명 계산 시 본문 원문과 일치해야 함).
/pg/settlements — 정산 요청정산 예정 잔액을 등록된 정산 계좌로 출금 신청합니다. idempotencyKey 필수(재전송·중복 호출 시 이중 정산 방지).
| 요청 필드 | 필수 | 설명 |
|---|---|---|
amount | Y | 정산(출금) 금액(원, 0 초과 · 정산 예정 잔액 이하) |
idempotencyKey | Y | 같은 정산 신청의 재시도를 구분하는 고유키 — 같은 키 재요청은 중복 처리되지 않음 |
{ "txnId": 80, "amount": 1000, "feeAmount": 0, "netAmount": 1000,
"balanceAfter": 10065, "scheduledAt": "2026-08-12T04:30:05" }
// netAmount : 수수료를 뺀 실지급액 · balanceAfter : 신청 후 남는 정산 예정 잔액
// scheduledAt : 실제 지급 예정 시각(정산 정책에 따라 정해집니다)
/pg/settlements — 정산 내역요청·처리중·완료 상태의 정산 내역을 최근순으로 돌려줍니다.
[ { "settlementId": 80, "amount": 1000, "feeAmount": 0,
"status": "PENDING", "statusLabel": "처리중",
"requestedAt": "2026-08-12T04:30:05", "completedAt": null } ]
| status | statusLabel | 뜻 |
|---|---|---|
CONFIRMED | 완료 | 계좌로 실제 입금됨(completedAt 채워짐) |
FAILED | 실패 | 이체 실패 — 금액이 정산 예정 잔액으로 복원됩니다 |
CANCELED | 취소 | 신청이 취소됨 |
| 그 외 | 처리중 | 접수·이체 진행 중(completedAt 은 null) |
결제 결과가 확정되면 등록한 주소로 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", 주문 상태는 CANCELED |
// 웹훅 본문 예 — returnInfo(주문 생성 때 보낸 임의 정보)가 그대로 되돌아옵니다(대사용)
{ "event":"PAYMENT_COMPLETED", "paymentId":1024, "orderNo":"SHOP-1001", "amount":11000,
"status":"PAID", "txnId":55231, "returnInfo":"SHOP-ORDER-1001" }
// 서명 검증(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 | 인증 실패 전반 — 서명 불일치·헤더 누락·시각 오차·재사용(replay)·미승인 화이트IP(HMAC·IP 재확인) |
| 404 | 주문 없음 (본인 매장의 주문이 아니면 조회되지 않아 404) |
참고: 인증·IP 실패는 모두 401로 통일 반환됩니다(별도 403 없음).
연동 코드를 AI 에게 맡기실 수 있습니다. 아래 상자를 통째로 복사해 쓰시는 AI(Claude·ChatGPT·Copilot 등)에 붙여 넣고, 맨 아래 <내 상황> 만 채우세요. 여기 적힌 내용은 실제로 동작하는 서버에서 뽑아낸 것이라 AI 가 없는 규칙을 지어내지 않습니다.
API_KEY·WEBHOOK_SECRET 은 절대 프롬프트에 적지 마세요.
열쇠는 서버 환경변수로만 넣고, AI 에게는 "환경변수에서 읽어 쓰라"고 시키면 됩니다.
아래 프롬프트도 그렇게 적혀 있습니다.당신은 쇼핑몰 서버에 NestPay PG 결제를 연동하는 개발자입니다.
아래 규격만 사용하고, 여기 없는 필드·엔드포인트·규칙을 임의로 만들지 마세요.
불확실하면 "규격에 없음"이라고 답하고 멈추세요.
# 1. 기본
- 기준 주소(Base URL): https://npwp-pg.nestpay.co.kr
- 모든 경로는 /pg 로 시작합니다.
- 요청·응답 본문은 JSON(UTF-8). 모든 시각은 KST(한국시간) 기준입니다.
- 공통 응답 형태: { "success": true|false, "data": {...}, "error": "실패 사유", "meta": null }
→ success 가 false 면 error 를 읽어 처리하세요. HTTP 200 이어도 success=false 일 수 있습니다.
# 2. 인증 (요청마다 4개 헤더)
- X-Client-Id : 발급받은 클라이언트 ID
- X-Api-Key : 발급받은 API 열쇠
- X-Timestamp : 현재 시각(Unix epoch 초, 문자열)
- X-Signature : 아래 서명값(소문자 16진수)
서명 만드는 법:
canonical = METHOD + "\n" + 경로(쿼리스트링 포함) + "\n" + X-Timestamp + "\n" + 요청본문원문
X-Signature = HMAC_SHA256(API_KEY, canonical) 를 소문자 16진수로
규칙:
- 본문이 없으면(GET 등) 본문 자리는 빈 문자열입니다.
- 본문은 해시하지 않고 "보낸 원문 그대로" 씁니다. 직렬화한 문자열을 그대로 전송해야 하며,
서명 계산에 쓴 문자열과 실제 전송 본문이 한 글자라도 다르면 401 이 납니다.
- 경로는 쿼리스트링까지 포함합니다. 예: /pg/payments?status=PAID&page=1
- X-Timestamp 는 서버 시각과 300초(5분) 이상 차이 나면 거부됩니다. 서버 시각을 NTP 로 맞추세요.
- API_KEY 는 환경변수에서 읽어 쓰고, 코드·로그·이 프롬프트에 절대 남기지 마세요.
- 발급받은 서버 IP 만 허용됩니다(화이트리스트). 미승인 IP 는 401 입니다.
# 3. 엔드포인트 (이게 전부입니다 — 8개)
POST /pg/ping
연결·서명·IP 설정 점검용. 본문은 빈 객체 {} 를 보냅니다.
응답 data: { "status":"OK", "merchantId":1, "mode":"SANDBOX"|"LIVE" }
→ 연동을 처음 붙일 때 이것부터 200 이 되게 만드세요.
POST /pg/payments (결제 주문 생성)
요청: {
"orderNo": "SHOP-1001", // 필수. 쇼핑몰 주문번호
"amount": 11000, // 결제 총액(원). items 를 보내면 생략 가능
"itemName": "아메리카노 외 1건", // items 를 보내면 생략 가능(자동 생성)
"vatIncluded": true, // 선택. 금액에 부가세가 포함되어 있는지
"items": [ // 선택. 다품목(최대 100개)
{ "name":"아메리카노", "qty":2, "unitPrice":4000, "taxFree":false }
],
"returnInfo": "SHOP-ORDER-1001" // 선택. 문자열(500자 이하). 그대로 되돌려 받습니다
}
응답 data: {
"paymentId":22, "orderNo":"SHOP-1001", "status":"PENDING",
"amount":11000, "supplyAmount":10273, "vatAmount":727, "itemName":"아메리카노 외 1건",
"qrData":"NPQR1.xxxxx", // QR 로 그릴 문자열(PC 웹)
"deeplink":"nestpay://pay?token=NPQR1.xxxxx", // 앱 열기(모바일 웹)
"expiresAt":"2026-08-12T04:32:37", "expiresInSeconds":299,
"returnInfo":"SHOP-ORDER-1001", "sandbox":false
}
주의: 같은 orderNo 로 다시 요청하면 새로 만들지 않고 기존 주문을 그대로 돌려줍니다(중복 생성 방지).
GET /pg/payments/{paymentId} (결제 단건 조회)
응답 data: {
"paymentId":22, "orderNo":"SHOP-1001", "status":"PENDING"|"PAID"|"EXPIRED"|"CANCELED",
"amount":11000, "supplyAmount":10273, "vatAmount":727, "itemName":"...",
"items":[ {"name","qty","unitPrice","amount","taxFree","supplyAmount","vatAmount"} ], // 없으면 null
"returnInfo":"SHOP-ORDER-1001",
"txnId":55231, // 결제 확정 시 생기는 거래번호(미결제면 null)
"paidAt":"2026-07-26T12:31:10", // 미결제면 null
"canceledAt":null, // 취소·환불 시각(없으면 null)
"sandbox":false
}
GET /pg/payments?status=&page=&limit= (결제 내역)
본인 매장 건만 조회됩니다. status 생략 시 전체.
응답 data: { "total":14, "page":1, "limit":20, "items":[ 단건조회와 같은 필드 + "createdAt" ] }
주의: 목록 항목에는 items(품목 목록)가 없습니다. 품목은 단건 조회로 확인하세요.
POST /pg/payments/{paymentId}/cancel (취소 / 환불)
본문은 빈 객체 {}. 주문 상태에 따라 동작이 갈립니다.
PENDING → 취소(QR 소각). 응답 { "paymentId":22, "status":"CANCELED", "refunded":false }
PAID → 전액 환불. 응답 { "paymentId":22, "status":"CANCELED", "refunded":true, "refundAmount":11000 }
CANCELED → 멱등. 그대로 CANCELED 를 돌려줍니다(다시 불러도 안전)
EXPIRED → 400 오류(만료된 주문은 취소 불가)
부분취소·부분환불은 없습니다(전액만).
GET /pg/balance (정산 예정 잔액)
응답 data: { "settlementBalance":452000, "paidCount":61, "paidSum":690000, "refundedCount":3 }
POST /pg/settlements (정산 요청)
요청: { "amount":1000, "idempotencyKey":"IDEM-20260812-01" } // idempotencyKey 필수
응답 data: { "txnId":80, "amount":1000, "feeAmount":0, "netAmount":1000,
"balanceAfter":10065, "scheduledAt":"2026-08-12T04:30:05" }
같은 idempotencyKey 로 재요청하면 중복 처리되지 않습니다(재전송 안전).
GET /pg/settlements (정산 내역)
응답 data: [ { "settlementId":80, "amount":1000, "feeAmount":0,
"status":"CONFIRMED"|"FAILED"|"CANCELED"|그외,
"statusLabel":"완료"|"실패"|"취소"|"처리중",
"requestedAt":"...", "completedAt":null } ]
# 4. 웹훅(콜백) 받기
NestPay 가 등록된 콜백 주소로 POST 합니다.
- 헤더 X-Nestpay-Event : 이벤트 종류
- 헤더 X-Nestpay-Signature : HMAC_SHA256(WEBHOOK_SECRET, 요청본문원문) 소문자 16진수
이벤트 종류:
PAYMENT_COMPLETED 결제 완료
PAYMENT_EXPIRED 유효시간 경과로 결제 실패
PAYMENT_CANCELED 미결제 주문 취소
PAYMENT_REFUNDED 결제완료 건 전액 환불
SIGNATURE_TEST 서명 검증 시험(아래 참고)
본문 예:
{ "event":"PAYMENT_COMPLETED", "paymentId":22, "orderNo":"SHOP-1001",
"amount":11000, "status":"PAID", "returnInfo":"SHOP-ORDER-1001" }
반드시 지킬 것:
1) 서명 검증을 먼저 하세요. 본문을 파싱하기 전에, 받은 원문 바이트 그대로 HMAC 을 계산해
X-Nestpay-Signature 와 비교합니다. 다르면 4xx 로 거절하고 아무 처리도 하지 마세요.
(검증하지 않으면 누구나 가짜 "결제 완료"를 보낼 수 있습니다 — 돈은 안 들어오고 물건이 나갑니다)
2) 비교는 타이밍 공격에 안전한 방식(constant-time)으로 하세요.
3) 멱등 처리하세요. 같은 (paymentId, event) 알림이 재시도로 여러 번 올 수 있습니다. 1회만 반영하세요.
4) 검증에 성공하고 처리했으면 2xx 로 답하세요. 2xx 가 아니면 NestPay 가 재시도합니다.
5) ★ 웹훅 본문의 status 를 주문 상태로 그대로 저장하지 마세요.
환불 알림의 status 는 "REFUNDED" 이지만, 조회 API 의 주문 상태는 "CANCELED" 입니다.
주문 상태는 조회 API 의 status 를 기준으로 삼고, 환불 여부는 event 로 판단하세요.
6) 웹훅이 유실될 수 있습니다. 조회 API 폴링을 함께 두어 최종 상태를 확정하세요.
# 5. 결제 흐름
1) 쇼핑몰 서버가 POST /pg/payments 로 주문 생성 → paymentId·qrData·deeplink·expiresAt 수신
2) PC 웹이면 qrData 로 QR 을 그려 보여 주고, 모바일 웹이면 deeplink 로 앱을 엽니다
3) 사용자가 NestPay 앱으로 결제
4) 웹훅(PAYMENT_COMPLETED) 수신 + GET /pg/payments/{paymentId} 로도 확인
5) 유효시간(expiresAt)이 지나면 자동 만료 → 웹훅(PAYMENT_EXPIRED), 조회 시 EXPIRED
# 6. 오류 처리
200 정상. 단 success=false 면 error 를 확인
400 필수값 누락·형식 오류·취소 불가 상태
401 인증 실패 전반 — 서명 불일치·헤더 누락·시각 오차·재사용·미승인 IP
404 주문 없음(본인 매장 주문이 아니면 404)
※ 인증·IP 실패는 모두 401 로 옵니다(403 은 쓰지 않습니다).
# 7. 샌드박스 / 라이브
- 주소와 열쇠가 같습니다. 승인된 모드에 따라 서버가 구분합니다.
- 지금 어느 모드인지는 /pg/ping 의 mode, 또는 조회 응답의 sandbox 로 확인하세요.
- ★ 샌드박스도 실제 결제입니다(모의 결제 아님). 손님 잔액이 실제로 빠집니다.
적은 금액으로 시험하고, 끝나면 취소하세요.
# 8. 라이브 전환에 필요한 시험 4종
① 결제 생성 : POST /pg/payments 성공
② 결제 조회 : GET /pg/payments/{id} 로 PAID 또는 CANCELED 상태를 읽기
(PENDING 만 조회하면 통과되지 않습니다)
③ 웹훅 수신 : PAYMENT_COMPLETED 를 받고 2xx 로 응답
④ 서명 검증 : NestPay 가 일부러 서명을 틀리게 만든 SIGNATURE_TEST 알림을 보냅니다.
4xx 로 거절해야 통과, 2xx 로 답하면 불통과입니다.
→ ④ 때문에라도 서명 검증은 반드시 구현해야 합니다.
# 9. 나에게 만들어 줄 것
- 위 규격대로 동작하는 연동 코드
- 서명 생성 함수와 웹훅 서명 검증 함수(테스트 포함)
- 웹훅 멱등 처리와 조회 폴링을 함께 둔 구조
- API_KEY·WEBHOOK_SECRET 은 환경변수에서 읽고, 로그에 남기지 않을 것
<내 상황>
- 사용 언어/프레임워크 :
- 쇼핑몰 주문 저장 방식 :
- 웹훅 받을 주소 :