개발자
Webhook
인보이스 상태가 바뀌면 CryBit가 가맹점의 webhook URL로 서명된 POST 요청을 JSON 본문과 함께 전송합니다.
설정
가맹점 설정의 연동 탭에서 webhook URL을 설정하고 paid, expired, refunded, cancelled 이벤트를 선택하세요. URL은 80, 443, 8080, 8443 포트를 사용하는 공개 HTTP 또는 HTTPS 주소여야 하며, 리디렉션은 따라가지 않습니다. 이는 인보이스 요청이 아닌 가맹점 단위로 설정됩니다.
헤더
| 헤더 | 값 |
|---|---|
X-CryBit-Event | 이벤트 이름입니다(예: payment.paid). |
X-CryBit-Signature | t=<unix time>,v1=<hex>: 서명 시크릿으로 "<t>.<raw body>"를 HMAC-SHA256으로 계산한 값입니다. |
서명 검증하기
서명 시크릿은 가맹점 설정의 webhook URL 옆에 표시됩니다. 원본 본문(raw body)으로 서명을 계산하고, 상수 시간 비교로 검증하며, 5분이 지난 요청은 거부하세요.
PHP
$secret = getenv('CRYBIT_WEBHOOK_SECRET');
$body = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_CRYBIT_SIGNATURE'] ?? '';
parse_str(str_replace(',', '&', $header), $p); // t=…&v1=…
$t = (int) ($p['t'] ?? 0);
$expected = hash_hmac('sha256', $t . '.' . $body, $secret);
if (abs(time() - $t) > 300 || !hash_equals($expected, $p['v1'] ?? '')) {
http_response_code(400);
exit;
}
// the request is genuine: store the event, answer 200, do the rest later
http_response_code(200);Node.js
import crypto from 'node:crypto'
export function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((kv) => kv.split('=')))
const t = Number(parts.t)
const expected = crypto.createHmac('sha256', secret).update(t + '.' + rawBody).digest('hex')
const ok = parts.v1 && parts.v1.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected))
return ok && Math.abs(Date.now() / 1000 - t) <= 300
}전송 및 재시도
- 2xx 상태 코드로 신속히 응답하세요. CryBit는 약 12초간 대기합니다.
- 응답이 없으면 CryBit는 15초, 1분, 5분, 15분, 30분 후, 그다음 2시간, 6시간, 24시간 후에 재시도합니다.
- 429를 제외한 4xx 응답을 받으면 재시도가 중단됩니다.
- 동일한 이벤트가 여러 번 도착할 수 있으므로, order_id나 인보이스 uuid를 키로 사용해 멱등하게 처리하세요.
- webhook을 놓쳤다면 GET /api/v1/payments/{uuid}로 상태를 조회하세요. 이 값이 항상 정확한 기준이 됩니다.
FAQ
전송 내역을 확인할 수 있나요?
네, 가능합니다. 가맹점 계정의 API 로그 페이지에서 요청과 webhook 전송 내역을 상태 코드 및 본문과 함께 확인할 수 있습니다.
