APIFuseAPIFuseDocs

Getting started

  • 개요
  • 빠른 시작

Authentication

  • Managed key
  • Scope

Errors

  • 에러 모델
  • 재시도

Webhooks

  • 개요
  • 서명 검증

API reference

  • 아마존 재팬
  • 배달의민족 프로바이더
  • 빗썸
  • Buyee
  • 캐치테이블 음식점 검색 및 예약
  • 차란 커머스
  • 당근 공개 매물
  • 다이소 상품 및 매장 데이터
  • 다나와 가격 비교
  • Demaecan
  • 에키탄
  • 여기어때 국내 숙소
  • Google Flights
  • 한강 수위
  • 핫페퍼 구루메(Hot Pepper Gourmet) 프로바이더
  • 현대카드
  • 인천공항 여객편 운항현황
  • Jalan
  • 일본 국회 회의록
  • 일본 재난 알림
  • 일본 EDINET 공시 서류
  • 일본 e-Gov 법령
  • 일본 e-Stat
  • 일본 gBizINFO (경제산업성 법인정보)
  • 일본 GSI 지오코딩
  • 일본 e-Gov 오픈데이터
  • Japan Post ZIP
  • 일본 공휴일
  • JMA Weather
  • 카카오맵 장소 검색과 길찾기
  • 카카오 T 택시 호출
  • 카카오톡 네이티브 메시징
  • 한국 주소 검색
  • AirKorea 실시간 대기오염 정보
  • 전국 동물병원
  • 한국 아파트 전월세 실거래가
  • 한국 아파트 매매 실거래가
  • 나라장터 입찰공고
  • 한국 박스오피스
  • 건축물대장 (Korea Building Register)
  • 전국 버스 도착정보
  • Korea Business Verify
  • 대한민국 캠핑장
  • 한국 DART 기업 재무정보
  • DART 기업 정보
  • 한국 문화행사(한눈에 보는 문화정보)
  • 한국 재난 알림
  • 한국 응급의료기관
  • 한국 ETF
  • 한국 전기차 충전소
  • 한국 환율 (한국수출입은행)
  • 한국 고속도로 교통
  • 금융상품 비교공시 (금융감독원 금융상품한눈에)
  • 한국 주유소 유가
  • 국가유산 정보 (국가유산청)
  • Korea Holiday
  • Korea Hospital Info
  • 전국 고속·시외버스 시간표
  • 한국 토지 매매 실거래가
  • 식약처 의약품 안전정보
  • 식약처 식품 안전 정보
  • 생활쓰레기 배출정보
  • 국가법령정보 검색·조회
  • NEIS 학교 급식 식단
  • 배송사 목록 및 택배 배송 조회
  • 전국 주차장
  • 공연예술 정보 (KOPIS)
  • Korea Pharmacy
  • 한국 인구 통계
  • 한국 주식시장지수
  • 한국 주식 시세
  • 전국 상가업소 정보
  • 한국 유기동물 (국가동물보호정보시스템)
  • 한국 관광정보 (한국관광공사)
  • 한국 열차 시간표
  • 한국 기상청 예보 데이터
  • Korea Weather Forecast
  • 대한민국 복지서비스
  • K-Startup
  • LH 청약 공고
  • 마켓컬리 상품 데이터
  • Mercari
  • 모두의주차장
  • 네이버 블로그 검색
  • 네이버 항공권 API
  • 네이버 지역 검색
  • 네이버 지도
  • 네이버 뉴스 검색
  • NHK 편성표
  • NOL 숙소
  • 오늘의집 스토어 및 콘텐츠
  • 라쿠텐 북스
  • 라쿠텐 이치바
  • 라쿠텐 레시피
  • 라쿠텐 트래블
  • SEC EDGAR Filings
  • 서울 따릉이
  • 서울 실시간 혼잡도
  • 서울 지하철 실시간 도착
  • 신한은행
  • 신한카드
  • Skiplagged
  • Steam 스토어
  • SUUMO
  • 스윙 택시
  • Tabelog
  • TableCheck
  • 업비트
  • Visit Japan Web (일본 입국 수속 서비스)
  • Weverse 프로바이더
  • 야후! 쇼핑 (일본)
  • Yogiyo
  • ZOZOTOWN

Guides

  • APIFuse 문서
  • 시작하기
  • 인증과 connection
  • 플레이그라운드
  • OpenAPI와 스키마 아티팩트
  • MCP 엔드포인트
  • 개발자 MCP 가이드
  • 스키마 번들과 변환 경고
  • Next.js App Router 연동
  • FastAPI 연동
  • 에러 처리
  • 자주 묻는 질문
  • 추가 리소스

Changelog

  • 체인지로그
APIFuseAPIFuse
문서서비스플레이그라운드상태
로그인회원가입

Webhooks

서명 검증

수신한 webhook payload의 서명을 검증하는 방법을 설명합니다.

webhook endpoint는 인터넷에 열려 있는 URL이고, body가 그럴듯하다는 사실은 아무것도 증명하지 않습니다. 서명이 검증되지 않은 payload는 위조, 변조, 재전송 어느 쪽도 배제할 수 없으므로, 모든 payload는 처리하기 전에 서명부터 검증합니다. 검증에 쓰는 endpoint secret은 whsec_ 접두사로 시작하며, endpoint를 등록할 때 콘솔에 한 번만 표시됩니다.

Frism-Signature 헤더

모든 전달에는 Frism-Signature 헤더가 붙습니다. 값은 ,로 구분된 key=value 쌍입니다.

전달 요청의 헤더
POST /webhooks/frism HTTP/1.1
Host: example.com
Content-Type: application/json
Frism-Event-Id: evt_01J8QH5R7T
Frism-Signature: t=1788257727,v2=d7e16839f487db479fa277fb11c2d6139225ea837c672df8a68e0452fd7ea772,v1=59ba753f14e68fd9aff609e623a55afd67db964cc881d66cb7aa09e3ef65ad58
부분의미
t서명이 생성된 시각입니다. unix 초 단위이며, 5분보다 오래된 이벤트는 거부합니다.
v2endpoint secret으로 <t>.<raw body> 문자열을 HMAC-SHA256 서명한 hex digest입니다. 검증 대상은 이 값입니다.
v1raw body만 서명한 legacy digest입니다. 2026-09-30까지만 함께 전송되며, 새 코드에서는 사용하지 않습니다.

v1은 timestamp를 포함하지 않아 가로챈 payload를 나중에 재전송해도 그대로 통과합니다. 항상 v2만 신뢰하고, v1 전송 중단 일정은 changelog에서 확인합니다.

검증 절차

  1. Frism-Signature 헤더를 ,로 나누어 t와 v2를 파싱합니다.
  2. t가 현재 시각 기준 5분 이내인지 확인합니다. 벗어나면 거부합니다.
  3. 수신한 raw body 문자열 그대로 <t>.<raw body>를 만들고, endpoint secret으로 HMAC-SHA256 digest를 계산합니다. JSON.parse 후 재직렬화한 body가 아니라 도착한 바이트 그대로여야 합니다.
  4. 계산한 digest와 v2를 timing-safe 방식으로 비교합니다. 일치할 때만 JSON을 파싱하고 처리를 시작합니다.

digest는 t와 body를 .로 이어붙인 문자열 위에서 계산되므로, timestamp와 body 중 어느 하나만 바꿔도 서명이 깨집니다. 이 결합이 재전송 공격을 막는 핵심입니다. t 검사에 5분의 여유를 두는 것은 전달 지연과 시계 오차를 흡수하기 위해서이므로, 서버 시계는 NTP로 맞춰 둡니다.

TypeScript 검증 예시

Node.js 내장 node:crypto만으로 충분합니다. 외부 라이브러리는 필요하지 않습니다.

raw body 기반 서명 검증
import { createHmac, timingSafeEqual } from "node:crypto";

const TOLERANCE_SECONDS = 5 * 60;

export function verifyFrismSignature(
  rawBody: string,
  header: string,
  secret: string,
): boolean {
  // 1. 헤더를 key=value 쌍으로 파싱합니다.
  const parts = new Map<string, string>();
  for (const pair of header.split(",")) {
    const index = pair.indexOf("=");
    if (index > 0) parts.set(pair.slice(0, index), pair.slice(index + 1));
  }
  const t = parts.get("t");
  const v2 = parts.get("v2");
  if (t === undefined || v2 === undefined) return false;

  // 2. timestamp가 5분 이내인지 확인합니다.
  const timestamp = Number(t);
  if (!Number.isInteger(timestamp)) return false;
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - timestamp) > TOLERANCE_SECONDS) return false;

  // 3. "<t>.<raw body>"에 대한 기대 digest를 계산합니다.
  const expected = createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest();

  // 4. timing-safe 비교. 길이가 다르면 timingSafeEqual이 던지므로 먼저 거릅니다.
  const received = Buffer.from(v2, "hex");
  if (received.length !== expected.length) return false;
  return timingSafeEqual(expected, received);
}

export async function handleFrismEvent(request: Request): Promise<Response> {
  // raw body를 먼저 확보합니다. JSON.parse는 검증에 성공한 뒤에만 합니다.
  const rawBody =  request.text();
   header = request.headers.get() ?? ;
   secret = process.env.FRISM_WEBHOOK_SECRET ?? ;

   (!verifyFrismSignature(rawBody, header, secret)) {
    
      Response(null, { status:  });
  }

   event = JSON.parse(rawBody) as { id: string; : string };
  
  console.log(`verified ${event.} (${event.id})`);
    Response(null, { status:  });
}

흔한 실수

실수무엇이 잘못되는가
검증 전에 JSON.parse파싱 후 재직렬화한 문자열은 공백과 키 순서가 달라져 digest가 절대 일치하지 않습니다. 검증되지 않은 데이터를 만지는 것 자체도 위험합니다. 항상 raw body 문자열로 검증합니다.
===로 digest 비교일반 문자열 비교는 불일치 위치에 따라 소요 시간이 달라져 timing 정보를 흘립니다. timingSafeEqual을 사용합니다.
v2 대신 v1 검증v1은 timestamp가 없어 오래된 payload의 재전송을 막지 못하고, 2026-09-30 이후에는 전송 자체가 중단됩니다.

검증에 실패하면

400을 반환하고 payload는 처리하지 않습니다. 검증 실패에 2xx를 반환하면 APIFuse은 전달이 성공한 것으로 기록하고 재시도를 멈추므로, 실패는 반드시 4xx로 알립니다. 로그에는 Frism-Event-Id와 실패 사유만 남기고 body 내용은 신뢰하지 않습니다. 배포 실수로 정상 이벤트를 거부하게 되더라도 APIFuse이 하므로, 검증 코드를 고치면 이벤트는 다시 도착합니다.

로컬 테스트

openssl로 직접 서명한 요청을 보내면 배포 전에 검증 코드를 확인할 수 있습니다. 아래 body는 를 담은 reservation.confirmed 이벤트입니다. 스크립트의 T나 BODY를 바꿔 가며 핸들러가 불일치 digest와 오래된 timestamp를 모두 거부하는지도 확인해 봅니다.

서명된 전달을 직접 만들어 보내기
SECRET="whsec_mJq82LkabQfw"   # 콘솔에서 발급받은 endpoint secret
BODY='{"id":"evt_01J8QH5R7T","type":"reservation.confirmed","created_at":"2026-09-01T10:15:27Z","data":{"object":{"id":"rsv_01J8QDGT2K","status":"confirmed","restaurant_id":"res_01J8Q9W3TE","starts_at":"2026-09-02T19:00:00+09:00","party_size":2}}}'
T=$(date +%s)
V2=$(printf "%s.%s" "$T" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')

curl -X POST http://localhost:3000/webhooks/frism \
  -H "Content-Type: application/json" \
  -H "Frism-Event-Id: evt_01J8QH5R7T" \
  -H "Frism-Signature: t=$T,v2=$V2" \
  --data "$BODY"
envelope 구조와 이벤트 종류, 중복 제거 방법은 에서 다룹니다.
await
const
"Frism-Signature"
""
const
""
if
// 검증 실패: 처리하지 않고 400으로 끝냅니다.
return
new
400
const
type
// 여기서부터는 신뢰할 수 있는 이벤트입니다.
type
return
new
200
24시간 동안 재시도
reservation 리소스
webhook 개요