결제 웹훅
서명된 결제·체크아웃·환불 이벤트를 판매자 백엔드에서 수신합니다.
RANTAN은 호스티드 결제, 체크아웃 또는 환불 상태가 바뀌면 판매자 백엔드로 서명된 HTTPS 이벤트를 전송합니다. 웹훅은 비동기 신호이므로 이벤트를 검증하고 멱등하게 처리하세요.
#Endpoint 등록
판매자 JWT로 HTTPS endpoint를 등록합니다. 사설망·loopback·로컬 네트워크·인증정보 포함 URL·리디렉션·비표준 포트는 허용하지 않습니다.
curl -X POST https://app.rantan.xyz/api/v2/webhooks/endpoints/ \
-H "Authorization: Bearer $RANTAN_SELLER_JWT" \
-H "Content-Type: application/json" \
-d '{
"url": "https://merchant.example/webhooks/rantan",
"description": "Production",
"subscribed_events": [
"payment.succeeded",
"checkout.completed",
"checkout.failed",
"refund.completed"
]
}'{
"id": "7d97ad12-7091-43f6-b9bc-707cab54ae29",
"url": "https://merchant.example/webhooks/rantan",
"description": "Production",
"subscribed_events": [
"payment.succeeded",
"checkout.completed",
"checkout.failed",
"refund.completed"
],
"is_active": true,
"created_at": "2026-07-30T10:00:00Z",
"updated_at": "2026-07-30T10:00:00Z",
"signing_secret": "whsec_store-this-once"
}#이벤트 유형
| 이벤트 | 전송 시점 |
|---|---|
payment.succeeded | x402 정산 내역이 영구 기록됐을 때 |
checkout.completed | RANTAN 이행 처리가 성공적으로 완료됐을 때 |
checkout.failed | 체크아웃이 최종 실패 상태가 됐을 때 |
checkout.canceled | 세션 또는 연결된 구매가 취소됐을 때 |
checkout.expired | 세션 또는 연결된 구매가 만료됐을 때 |
refund.completed | 환불 완료가 기록됐을 때 |
#이벤트 Payload
모든 이벤트에는 고정된 id가 있습니다. 접근 권한, 재고, 크레딧 또는 주문 상태를 변경하기 전에 이 값을 고유 키로 저장하세요.
{
"id": "d22f4de0-4e92-4dc9-af9f-aaadbbdad801",
"type": "payment.succeeded",
"api_version": "2026-07-30",
"created_at": "2026-07-30T10:04:32Z",
"data": {
"object": {
"checkout_session_id": "c8a9d48e-68b2-48dd-a754-75635cae71d6",
"client_reference_id": "order_20260730_001",
"product_id": 123,
"purchase_id": "bfaf2aa4-dd86-4356-92db-ae0431806867",
"purchase_status": "payment_confirmed",
"completed": false,
"buyer_wallet_address": "0x1234...abcd",
"quantity": 1,
"currency": "USDC",
"total_amount": "12.50000000",
"amount_atomic": "12500000"
},
"payment": {
"provider": "x402",
"transaction_hash": "0xabcd...",
"amount_atomic": "12500000",
"recipient": "0x9876...fedc"
}
}
}#서명 검증
Rantan-Signature는 t={Unix timestamp},v1={HMAC-SHA256} 형식입니다. 정확한 raw body를 `${t}.${rawBody}`로 연결해 digest를 계산하고, 5분이 지난 timestamp를 거부한 뒤 상수 시간으로 비교하세요.
import { createHmac, timingSafeEqual } from "node:crypto";
export async function handleRantanWebhook(req, res) {
// Read the raw bytes before JSON parsing.
const rawBody = await readRawBody(req);
const signatureHeader = req.headers["rantan-signature"];
if (typeof signatureHeader !== "string") {
return res.status(400).end();
}
const parts = Object.fromEntries(
signatureHeader.split(",").map((part) => part.split("=")),
);
const timestamp = Number(parts.t);
if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > 300) {
return res.status(400).end();
}
const expected = createHmac(
"sha256",
process.env.RANTAN_WEBHOOK_SECRET,
)
.update(`${timestamp}.`)
.update(rawBody)
.digest("hex");
const received = Buffer.from(parts.v1 ?? "", "hex");
const calculated = Buffer.from(expected, "hex");
if (
received.length !== calculated.length ||
!timingSafeEqual(received, calculated)
) {
return res.status(401).end();
}
const event = JSON.parse(rawBody.toString("utf8"));
await processEventOnce(event.id, event.type, event.data);
return res.status(204).end();
}#재시도와 전송 이력
10초 안에 2xx 상태를 반환하세요. 리디렉션은 따라가지 않습니다. 실패 시 1분, 5분, 30분, 2시간, 6시간, 24시간, 48시간 뒤 재시도하며 총 8회 실패하면 failed 상태가 됩니다.
#수동 재시도
최근 전송 이력을 최대 100건 조회하고 완료 또는 실패 이벤트를 다시 보낼 수 있습니다. 이벤트 id는 유지되므로 수신 처리는 반드시 멱등해야 합니다.
curl -X POST \
https://app.rantan.xyz/api/v2/webhooks/deliveries/$DELIVERY_ID/retry/ \
-H "Authorization: Bearer $RANTAN_SELLER_JWT"#Webhook API
/api/v2/webhooks/endpoints/Endpoint를 만들고 최초 signing_secret을 받습니다.
/api/v2/webhooks/endpoints/판매자의 endpoint 목록을 조회합니다.
/api/v2/webhooks/endpoints/{endpointId}/URL, 구독 이벤트 또는 활성 상태를 변경합니다.
/api/v2/webhooks/endpoints/{endpointId}/특정 endpoint의 설정을 조회합니다.
/api/v2/webhooks/endpoints/{endpointId}/Endpoint를 비활성화하고 이후 전송을 중단합니다.
/api/v2/webhooks/endpoints/{endpointId}/rotate-secret/기존 비밀키를 폐기하고 새 signing_secret을 받습니다.
/api/v2/webhooks/deliveries/전송 상태와 응답 이력을 조회합니다.
/api/v2/webhooks/deliveries/{deliveryId}/retry/동일한 event id로 전송을 다시 시도합니다.