웹훅
서명된 POST 요청으로 실시간 결제 이벤트를 수신하세요. Crypax는 HMAC-SHA256 서명과 함께 웹훅을 전달하므로 처리 전에 신뢰성을 검증할 수 있습니다.
개요
결제 상태가 변경되면 Crypax가 설정된 웹훅 URL로 POST 요청을 보냅니다. 10초 이내에 2xx 상태 코드로 응답해야 합니다. 전송에 실패하면 최대 4회까지 재시도합니다. 웹훅 URL은 대시보드의 웹훅 메뉴에서 설정하세요.
이벤트 종류
Crypax는 아래 이벤트를 발생시킵니다. 개별 이벤트를 구독하거나 비워두면 모든 이벤트를 수신합니다.
| 이벤트 | 이름 | 발생 시점 |
|---|---|---|
payment.created | 결제 생성 | 새 결제가 생성됨. |
payment.processing | 결제 처리 중 | 트랜잭션 해시가 제출되어 온체인 확인 대기 중. |
payment.confirmed | 결제 확인 | 온체인에서 트랜잭션이 검증됨. 주문을 처리해도 안전합니다. |
payment.failed | 결제 실패 | 트랜잭션이 제출되었지만 온체인 검증에 실패함. |
payment.expired | 결제 만료 | expiresAt 이전에 결제가 확인되지 않음. |
payment.refunded | 결제 환불 | 이 결제에 대한 환불이 완료됨. |
refund.created | 환불 생성 | 새 환불이 시작됨. |
refund.completed | 환불 완료 | 환불 트랜잭션이 온체인에서 확인됨. |
refund.failed | 환불 실패 | 환불 트랜잭션이 실패함. |
settlement.requested | 정산 요청 | 정산이 요청됨. |
settlement.approved | 정산 승인 | 정산이 승인됨. |
settlement.completed | 정산 완료 | 정산 트랜잭션이 확인됨. |
settlement.failed | 정산 실패 | 정산 트랜잭션이 실패함. |
customer.created | 고객 생성 | 새 고객 레코드가 생성됨. |
customer.updated | 고객 업데이트 | 고객 레코드가 업데이트됨. |
페이로드 구조
Crypax는 JSON 바디와 함께 두 개의 헤더를 전송합니다: 인증용 X-Crypax-Signature와 이벤트 유형용 X-Crypax-Event.
POST /your-webhook-endpoint HTTP/1.1
Content-Type: application/json
X-Crypax-Signature: v1=a4b3c2d1e0f9...
X-Crypax-Event: payment.confirmed
X-Crypax-Timestamp: 1704067200
{
"id": "pay_01HZ...",
"status": "confirmed",
"txHash": "0xabcdef1234...",
"blockNumber": 12345,
"amount": "10.00",
"currency": "native",
"chainId": 41956
}페이로드 필드
| 필드 | 타입 | 설명 |
|---|---|---|
id | string | 고유 결제 ID |
status | string | 이벤트 발생 시점의 결제 상태 |
txHash | string | null | 온체인 트랜잭션 해시 (미제출 시 null) |
blockNumber | number | null | 트랜잭션이 포함된 블록 번호 (미확인 시 null) |
amount | string | 최소 단위의 금액 (PLM은 wei) |
currency | string | PLM은 'native', ERC20은 컨트랙트 주소 |
orderId | string | null | 결제 생성 시 전달한 내부 주문 ID |
서명 검증
모든 웹훅 요청에는 X-Crypax-Signature 헤더가 포함됩니다. 이벤트를 처리하기 전에 반드시 서명을 검증하여 요청이 Crypax에서 온 것인지 확인하세요.
| 헤더 | 설명 |
|---|---|
X-Crypax-Signature | v1={hex} — {timestamp}.{rawBody}를 웹훅 시크릿으로 서명한 HMAC-SHA256 값 |
X-Crypax-Event | 이벤트 유형, 예: payment.confirmed |
X-Crypax-Timestamp | 이벤트가 전송된 Unix 타임스탬프(초). 오래된 요청을 거부하는 데 사용하세요. |
검증 알고리즘
서명 대상 페이로드는 timestamp + '.' + rawBody입니다. 웹훅 시크릿을 키로 이 문자열의 HMAC-SHA256을 계산하고 hex로 인코딩한 뒤, 시그니처 헤더의 v1= 이후 값과 비교합니다. 타이밍 공격 방지를 위해 crypto.timingSafeEqual을 사용하세요. 5분 이상 오래된 타임스탬프는 거부하세요.
const crypto = require('crypto');
function verifyWebhookSignature(rawBody, signatureHeader, timestampHeader, secret) {
// signatureHeader format: "v1=<hex>"
const signedPayload = timestampHeader + '.' + rawBody;
const expected = 'v1=' + crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Reject stale requests (older than 5 minutes)
const age = Math.floor(Date.now() / 1000) - Number(timestampHeader);
if (age > 300) {
throw new Error('Webhook timestamp too old');
}
return crypto.timingSafeEqual(
Buffer.from(signatureHeader),
Buffer.from(expected)
);
}핸들러 예제
웹훅 핸들러는 원시 바디를 파싱하고, 서명을 검증한 뒤, 즉시 200으로 응답해야 합니다. 무거운 작업은 백그라운드 큐로 미루세요.
Express.js (직접 검증)
const express = require('express');
const crypto = require('crypto');
const app = express();
app.post('/webhooks/crypax', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-crypax-signature'];
const timestamp = req.headers['x-crypax-timestamp'];
const eventType = req.headers['x-crypax-event'];
const secret = process.env.CRYPAX_WEBHOOK_SECRET;
// Build signed payload: timestamp.rawBody
const signedPayload = timestamp + '.' + req.body.toString();
const expected = 'v1=' + crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(400).json({ error: 'Invalid signature' });
}
const payload = JSON.parse(req.body.toString());
switch (eventType) {
case 'payment.confirmed':
console.log('Payment confirmed:', payload.id, payload.txHash);
break;
case 'payment.failed':
console.log('Payment failed:', payload.id);
break;
case 'payment.expired':
console.log('Payment expired:', payload.id);
break;
case 'refund.completed':
console.log('Refund completed:', payload.id);
break;
}
res.json({ received: true });
});Express.js (@crypax/node SDK 사용)
Node.js SDK는 서명을 검증하고 타입이 지정된 이벤트 객체를 반환하는 <code>constructEvent</code> 헬퍼를 제공합니다.
import { Crypax } from '@crypax/node';
import express from 'express';
const app = express();
const crypax = new Crypax('sk_live_...');
app.post('/webhooks/crypax', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-crypax-signature'] as string;
try {
const event = crypax.webhooks.constructEvent(
req.body, // raw body string
signature,
process.env.CRYPAX_WEBHOOK_SECRET!,
);
switch (event.type) {
case 'payment.confirmed':
console.log('Confirmed:', event.data.id, event.data.txHash);
break;
case 'payment.expired':
console.log('Expired:', event.data.id);
break;
case 'refund.completed':
console.log('Refund completed:', event.data.id);
break;
case 'customer.created':
console.log('New customer:', event.data.id);
break;
}
res.json({ received: true });
} catch (err) {
res.status(400).json({ error: 'Invalid signature' });
}
});재시도 정책
엔드포인트가 10초 이내에 2xx 상태로 응답하지 않으면 Crypax가 다음 일정으로 재전송합니다:
| 시도 | 대기 시간 | 비고 |
|---|---|---|
| 1 | 0s | 최초 전송 |
| 2 | 1s | 1차 재시도 |
| 3 | 5s | 2차 재시도 |
| 4 | 30s | 마지막 재시도 — 이후 추가 시도 없음 |
2xx 응답을 받으면 재시도가 중단됩니다. 각 시도는 대시보드 웹훅 상세 페이지의 전송 로그에 기록됩니다.
권장 사항
- 서명 검증에는 원시 바디를 사용하세요.
express.raw()또는 동등한 미들웨어를 사용하세요. JSON을 먼저 파싱하면 바이트 표현이 달라져 서명이 맞지 않습니다. - 타이밍 안전 비교를 사용하세요. 타이밍 사이드채널 공격 방지를 위해
===대신crypto.timingSafeEqual을 사용하세요. - 핸들러를 멱등하게 만드세요. 동일한 이벤트가 두 번 이상 전달될 수 있습니다.
id필드로 중복을 제거하세요. - 즉시 200으로 응답하세요. DB 쓰기나 다운스트림 호출을 기다리지 마세요. 먼저 응답하고 비동기로 처리하세요.
- HTTPS 엔드포인트만 허용됩니다. Crypax는
https://를 사용하지 않는 웹훅 URL을 거부합니다.