Webhook 가이드

FakeToss는 결제 상태, 가상계좌 입금, 취소 상태를 웹훅으로 전송합니다. 페이로드의 `secret` 값을 저장된 웹훅 시크릿과 비교해 검증하세요.

Payload format

{
  "eventType": "PAYMENT_STATUS_CHANGED",
  "createdAt": "2026-04-21T10:20:00+09:00",
  "secret": "whsec_live_or_test_value",
  "data": { ... }
}

PAYMENT_STATUS_CHANGED

{
  "eventType": "PAYMENT_STATUS_CHANGED",
  "createdAt": "2026-04-21T10:20:00+09:00",
  "secret": "whsec_123",
  "data": {
    "paymentKey": "pay_123",
    "orderId": "order-001",
    "status": "DONE",
    "approvedAt": "2026-04-21T10:19:58+09:00"
  }
}

DEPOSIT_CALLBACK

{
  "eventType": "DEPOSIT_CALLBACK",
  "createdAt": "2026-04-21T10:30:00+09:00",
  "secret": "whsec_123",
  "data": {
    "paymentKey": "pay_va_123",
    "accountNumber": "140-123-456789",
    "depositedAmount": 25000,
    "status": "DONE"
  }
}

CANCEL_STATUS_CHANGED

{
  "eventType": "CANCEL_STATUS_CHANGED",
  "createdAt": "2026-04-21T11:00:00+09:00",
  "secret": "whsec_123",
  "data": {
    "paymentKey": "pay_123",
    "status": "CANCELED",
    "cancelAmount": 15000,
    "reason": "고객 요청"
  }
}

Signature verification

HMAC 서명이 아니라 본문 안의 `secret` 필드를 검증합니다. 문자열 비교는 `timingSafeEqual`을 사용해 길이와 값을 함께 확인하세요.

import { NextRequest, NextResponse } from "next/server";
import { timingSafeEqual } from "node:crypto";

function safeEqual(a: string, b: string) {
  const left = Buffer.from(a);
  const right = Buffer.from(b);
  return left.length === right.length && timingSafeEqual(left, right);
}

export async function POST(request: NextRequest) {
  const payload = await request.json();
  const webhookSecret = process.env.FAKETOSS_WEBHOOK_SECRET || "";

  if (!safeEqual(payload.secret || "", webhookSecret)) {
    return NextResponse.json({ code: "INVALID_SECRET" }, { status: 401 });
  }

  return NextResponse.json({ received: true });
}

Retry schedule

전송 실패 시 1분, 4분, 16분, 64분, 256분 간격으로 재시도하며 최대 5회까지 보냅니다.

수동 발송

관리자 대시보드에서 개별 결제 이벤트를 직접 다시 보낼 수 있습니다. 운영 점검이나 리시버 디버깅 시 /admin/webhooks의 `Fire webhook` 버튼을 사용하세요.