Nomba 웹훅은(는) 무엇을 하나요?
웹훅은 Nomba가 서버에 실제로 자금이 이동했음을 알리는 방법입니다. 인터넷상의 누구나 웹훅 URL에 POST할 수 있으므로, 핸들러는 주문이 결제되었거나 환불되었다고 표시하기 전에 각 요청이 Nomba에서 왔음을 증명해야 합니다. 또한 Nomba가 동일한 이벤트를 재시도하는 상황도 처리해야 합니다.
HMAC을 직접 구현하지 말고 공식 SDK의 검증기를 사용하세요. Nomba의 서명은 :로 연결된 특정 필드를 포함하며, SDK는 단순한 구현이 놓칠 수 있는 재생 공격도 차단합니다.
| 언어 | 검증 방법 |
|---|---|
Node.js / TS (@nombaoss/nomba-node) | verifyWebhook(rawBody, headers, signingKey) |
Python (nomba) | from nomba.webhooks import verify_signature → verify_signature(raw_body, headers, signing_key) |
PHP (nombaoss/nomba-php) | Nomba\Webhook::verify($rawBody, $headers, $signingKey) |
각 언어의 정확한 임포트와 인수 타입은 references/sdk-setup.md에 나와 있습니다. 각 함수는 파싱된 이벤트를 반환하거나 예외(WebhookVerificationError / WebhookVerificationException)를 던집니다. 이 함수들은 API 자격 증명이 필요하지 않으며, Nomba 대시보드(개발자 → 웹훅 설정)의 웹훅 서명 키(signing key) 만 필요합니다. 환경 변수(예: NOMBA_WEBHOOK_SIGNING_KEY)에서 읽어오세요. 이는 API 클라이언트 시크릿과는 다른 비밀 키입니다.
핸들러, 단계별로
- 원본 바디를 가져옵니다. Nomba가 보낸 정확한 바이트를 검증하세요. JSON 바디 파서가 먼저 실행되고 객체를 다시 직렬화하면 서명이 일치하지 않을 수 있으며, 더 나쁜 경우 실제로 처리하는 것과 다른 내용을 검증하게 될 수 있습니다. 각 프레임워크마다 원본 바디를 가져오는 방법이 다릅니다. 올바른 방법은 references/frameworks.md를 참조하고, 해당 경로를 CSRF 보호에서 제외하세요(프레임워크에 해당 기능이 있는 경우).
- 검증 후 파싱합니다. 검증기가 반환한 이벤트를 사용하세요. 실패 시
401(또는400)을 응답하고 중단하세요. info 레벨에서 거부된 요청의 전체 바디나 헤더를 로깅하지 마세요. requestId로 중복을 제거합니다. Nomba는 전달을 재시도하므로, 동일한 이벤트가 두 번 이상 도착할 수 있습니다. 처리된 각requestId를 고유 제약 조건과 함께 기록하고, 이미 본requestId는 건너뛰세요. 중복 요청에도200을 반환해야 합니다. 그렇지 않으면 Nomba가 계속 재시도합니다.- 빠르게 응답을 확인합니다. 즉시
200을 반환하고, 느린 작업(이메일 발송, 주문 처리, 원장 업데이트 등)은requestId를 키로 하는 백그라운드 잡에서 처리하세요. 느린 핸들러는 타임아웃과 재전송을 유발합니다. - 아래 표를 사용하여 이벤트에 따라 조치합니다. 이벤트를 참조 번호로 자신의 레코드와 매칭하고, 금액과 통화가 예상과 일치하는지 확인한 후 주문을 처리하세요.
각 이벤트의 의미
event_type | 매칭 기준 | 조치 |
|---|---|---|
payment_success | 체크아웃: data.order.orderReference; 가상 계좌 / 이체 입금: data.transaction.aliasAccountReference 또는 transactionId | data.transaction.transactionAmount(및 통화)를 주문과 확인한 후 결제 완료로 표시합니다. 토큰화된 체크아웃의 경우 data.tokenizedCardData.tokenKey(존재하고 "N/A"가 아닌 경우)를 고객과 함께 서버 측에만 저장합니다. |
payment_failed | 위와 동일 | 시도 실패로 표시하고, 고객이 다시 시도하도록 합니다. 요금이 청구되지 않았습니다. |
payment_reversal | 위와 동일 | 받은 자금이 취소되었습니다. 주문 처리를 취소하거나 검토를 위해 플래그를 지정합니다. |
payout_success | data.transaction.merchantTxRef | 이체 또는 청구서 결제가 완료되었음을 표시합니다. |
payout_failed | data.transaction.merchantTxRef | 아직 환불하지 마세요. Nomba가 다른 경로를 통해 재시도할 수 있습니다. 실패로 표시하고 기다리세요. |
payout_refund | data.transaction.merchantTxRef | 자금이 Nomba 지갑으로 반환되었습니다. 이제 환불하거나 고객에게 크레딧을 제공하고 지급 실패로 표시해도 안전합니다. |
payout_failed(또는 타임아웃 시)에 환불하는 대신 payout_refund에서 환불해야 상인이 고객에게 두 번 지불하는 고전적인 실수를 방지할 수 있습니다. 인식할 수 없는 이벤트의 경우 로그를 남기고 200을 반환하세요.
API로 금액 결정을 확인하세요. Nomba의 서명은 event_type, requestId, data.merchant.userId, data.merchant.walletId, data.transaction.transactionId, type, time, responseCode 및 타임스탬프만 포함합니다. 금액, orderReference, merchantTxRef 및 카드 tokenKey는 서명되지 않습니다. 따라서 주문 처리, 환불 또는 카드 토큰 저장 전에 SDK를 사용하여 재조회하고 API의 응답에 따라 조치하세요: 체크아웃 결제의 경우 transactions.get({ orderReference }) (프로덕션에서는 checkout.getTransaction도 사용 가능), 지급의 경우 transactions.get({ merchantTxRef }). 서명된 requestId로 중복을 제거하면 이미 처리한 이벤트의 변조된 복사본을 방지하고, 재조회가 나머지를 보장합니다.
재생 보호
requestId로 중복을 제거하는 것이 주요 방어책입니다: 이미 처리한 이벤트의 재전송 또는 재시도 전달은 아무것도 변경하지 않습니다.
검증기는 오래된 타임스탬프를 거부할 수도 있지만(toleranceSeconds / tolerance_seconds / PHP의 네 번째 인수), 기본적으로는 비활성화하는 것이 좋습니다. Nomba는 nomba-timestamp를 페이로드가 전송된 시간으로 문서화하지만, 자동 재시도 시 새 타임스탬프가 생성되는지는 명시하지 않습니다. 만약 원래 타임스탬프가 유지된다면, 허용 오차가 Nomba의 재시도 기간보다 짧으면 장애 후 합법적인 재시도를 거부하여 유료 주문이 확인되지 않을 수 있습니다. 샌드박스에서 재전달 시 새 타임스탬프가 전달되는지 확인한 후에만 활성화하고, 서버 시계가 동기화된 상태에서 충분히 여유 있게(예: 1시간) 설정하세요.
예제 (Node.js + Express)
import express from "express";
import { verifyWebhook, WebhookEventType } from "@nombaoss/nomba-node";
const app = express();
// 이 경로에 대해서만 원시 바디 사용; 앱 전체 express.json()보다 먼저 등록하세요.
app.post("/webhooks/nomba", express.raw({ type: "*/*" }), async (req, res) => {
let event;
try {
event = verifyWebhook(req.body, req.headers, process.env.NOMBA_WEBHOOK_SIGNING_KEY!);
} catch {
return res.sendStatus(401);
}
// request_id에 대한 고유 제약 조건; 이미 본 경우 false 반환
const isNew = await webhookEvents.recordOnce(event.requestId, event.event_type);
if (isNew) await jobs.enqueue("nomba-webhook", event); // 백그라운드에서 처리
res.sendStatus(200);
});
Python (Flask/FastAPI/Django), PHP (Laravel/일반) 및 기타 Node 프레임워크는 references/frameworks.md에 있습니다.
테스트
- 단위 테스트: SDK 문서화된 테스트 벡터를 사용하여 핸들러를 테스트하세요: SDK의
webhook-fixture.json에서 바디, 키SampleSignatureKey123, 헤더nomba-timestamp: 2025-09-29T10:51:44Z,nomba-signature: q30yFZDyykoymBf9+mt1wz5UAuLk57raPGPHlGzIv00=. 잘못된 키, 변조된 바디, 누락된 헤더가 거부되는지, 그리고 중복된requestId가 한 번만 처리되는지 확인하세요. - 종단 간: 대시보드에서 테스트 웹훅 URL과 서명 키를 설정하고, 터널을 사용하여 로컬 서버를 노출시킨 후 샌드박스 결제를 해보세요. 대시보드에서 전달을 다시 푸시하거나 디버깅할 수 있습니다 (https://developer.nomba.com/docs/api-basics/troubleshoot-webhooks).
체크리스트
- 원시 바디가 비즈니스 로직이나 파싱 전에 SDK로 검증됩니다.
- 서명 키는 코드가 아닌 환경 변수에서 가져옵니다.
- 잘못된 서명은 401/400을 반환합니다; 다른 작업은 수행되지 않습니다.
-
requestId는 고유 제약 조건으로 중복 제거됩니다; 중복에도 200을 반환합니다. - 무거운 작업은 백그라운드에서 실행됩니다; 핸들러는 빠르게 200을 반환합니다.
- 주문 처리 전에 금액과 통화를 주문과 비교하여 확인합니다.
- 환불은
payout_refund(또는 재조회 결과REFUND반환)에서만 진행됩니다. - 경로는 CSRF 보호에서 제외되고 HTTPS를 사용합니다.