개요
키오스크 주문앱이 결제(VCAT 단말 승인)를 마치면 주문 전체를 플랫폼으로 보냅니다. 플랫폼은 금액을 다시 계산해 검증하고, 같은 주문은 한 번만 저장하며, 가맹점 서버로 웹훅을 보냅니다. 카드 승인·취소 자체는 단말에서 일어나고 플랫폼은 그 결과를 기록·통보합니다.
- Base URL:
https://<host>/api/v1 - 본문은 JSON (UTF-8), 금액은 원 단위 정수, 시각은 ISO 8601
- 주문 식별자
receiptId(rcpt_…)는 플랫폼이 발급,externalId는 앱의 주문 id
인증
콘솔 > 개발자에서 발급한 clientKey / secretKey를 HTTP Basic 으로 보냅니다. secret 은 발급 때 한 번만 보이며 서버(또는 단말 보안 저장소)에만 둡니다. 연동키마다 IP 허용목록과 만료일을 둘 수 있습니다.
Authorization: Basic base64(clientKey + ":" + secretKey)
연결 확인: GET /api/v1/ping?deviceId=dev_… → { ok, merchant, device, deviceFound }
주문 전송 — POST /orders
주문이 만들어지거나 바뀔 때(취소 등)마다 주문 객체 전체를 보냅니다. 같은 (deviceId, order.id) 는 몇 번 보내도 한 건이고, 결제완료 주문이 status: "canceled" 로 다시 오면 취소를 반영합니다. 실패하면 그대로 다시 보내면 됩니다(멱등).
curl -X POST https://<host>/api/v1/orders \ -u "ck_demo_client_key:sk_demo_secret_key_change_me" \ -H "content-type: application/json" \ -d @order.json
{
"deviceId": "dev_demo_gn1",
"order": {
"id": "1762671234567",
"orderNo": 42,
"createdAt": "2026-11-09T08:30:12.000Z",
"dineOption": "here",
"lines": [
{ "menuId": "AME", "name": "아메리카노", "unitPrice": 4500, "quantity": 2, "taxFree": false }
],
"amounts": { "total": 9000, "vat": 818, "taxFree": 0 },
"payMethod": "card",
"installment": 0,
"status": "paid",
"driver": "plus",
"approval": {
"approvalNo": "30012345",
"approvalDateTime": "261109173012",
"issuerName": "신한카드",
"acquirerName": "신한카드",
"cardBin": "45184200",
"cardKind": "신용",
"merchantNo": "00123456789",
"catId": "2393300001",
"tranSerial": "123456789012",
"mgmtNo": "0000000001",
"message": "정상승인"
}
}
}amounts는 서버가lines로 다시 계산해 비교합니다. 다르면AMOUNT_MISMATCH. (과세분 부가세 = 과세분 − round(과세분 / 1.1))- 카드·현금영수증은
approval필수, 취소 상태면cancel필수 - 현금영수증 식별번호·카드번호는 마스킹해서만 저장합니다.
- 같은 id 로 금액·결제수단·승인번호가 다른 주문이 오면
ORDER_CONFLICT
HTTP/1.1 201 Created
{
"result": "created", // created | canceled | duplicate
"order": {
"receiptId": "rcpt_k3j9x0q2...",
"externalId": "1762671234567",
"status": "paid",
"payMethod": "card",
"amounts": { "total": 9000, "vat": 818, "taxFree": 0 },
"approval": { "approvalNo": "30012345", "approvedAt": "2026-11-09T08:30:12.000Z", ... },
...
}
}주문 조회
GET /api/v1/orders/{receiptId}
GET /api/v1/orders?from=2026-11-01T00:00:00%2B09:00&to=2026-11-10T00:00:00%2B09:00&status=paid&storeId=str_…&limit=50&cursor=rcpt_…목록은 주문일시 내림차순, nextCursor 가 있으면 cursor 로 넘겨 다음 페이지를 받습니다. 웹훅을 받은 뒤 단건 조회로 값을 다시 확인하는 것을 권장합니다.
취소 반영 — POST /orders/{receiptId}/cancel
주문앱이 아닌 서버에서 취소 결과를 기록할 때 씁니다. (주문앱은 취소된 주문을 POST /orders 로 다시 보내면 됩니다.)
{ "approvalNo": "30019999", "approvalDateTime": "261109180000", "canceledAt": "2026-11-09T09:00:00Z" }웹훅
이벤트: order.paid, order.canceled. 2xx 응답이면 성공, 그 외·10초 초과는 실패로 보고 1분 · 5분 · 30분 · 2시간 · 6시간 간격으로 다시 보냅니다. 같은 이벤트가 두 번 갈 수 있으니 id(whd_…)로 중복을 거르세요.
POST https://example.com/webhooks/wavpay
x-wavpay-event: order.paid
x-wavpay-delivery: whd_...
x-wavpay-signature: t=1762671234,v1=5f2c...e9
{
"id": "whd_...",
"event": "order.paid",
"createdAt": "2026-11-09T08:30:13.000Z",
"data": { "order": { "receiptId": "rcpt_...", ... } }
}서명 검증 (Node.js):
import { createHmac, timingSafeEqual } from 'node:crypto';
// rawBody: 파싱 전 원문 문자열, secret: 콘솔의 whsec_...
export function verify(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false; // 재전송 공격 방지
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const a = Buffer.from(expected), b = Buffer.from(parts.v1 ?? '');
return a.length === b.length && timingSafeEqual(a, b);
}에러 코드
에러 응답: { "errorCode": "AMOUNT_MISMATCH", "message": "…", "details": … }
| errorCode | HTTP | 의미 |
|---|---|---|
AUTH_REQUIRED | 401 | Authorization 헤더가 필요합니다. (Basic base64(clientKey:secretKey)) |
AUTH_INVALID | 401 | 연동키가 올바르지 않습니다. |
KEY_EXPIRED | 401 | 만료되었거나 폐기된 연동키입니다. |
IP_BLOCKED | 403 | 허용되지 않은 IP 입니다. 연동키의 IP 허용목록을 확인하세요. |
INVALID_JSON | 400 | 요청 본문이 JSON 이 아닙니다. |
VALIDATION_FAILED | 422 | 요청 값이 올바르지 않습니다. |
AMOUNT_MISMATCH | 422 | 주문 항목 합계와 결제 금액이 다릅니다. |
DEVICE_NOT_FOUND | 404 | 등록되지 않은 단말입니다. 콘솔 > 매장·단말에서 단말 id 를 확인하세요. |
ORDER_NOT_FOUND | 404 | 주문을 찾을 수 없습니다. |
ORDER_CONFLICT | 409 | 같은 주문 id 로 다른 내용의 주문이 이미 들어와 있습니다. |
ALREADY_CANCELED | 409 | 이미 취소된 주문입니다. |
INTERNAL | 500 | 서버 오류입니다. 잠시 후 다시 시도하세요. |
재시도 기준: 5xx·네트워크 오류는 재시도, 4xx는 요청을 고쳐야 하므로 재시도하지 않습니다.