웹훅 받기
서명 검증 · 중복 거르기 · 순서 · 재시도 · 터널로 로컬 개발
주문 · 입금에 변화가 생기면 등록한 주소로 JSON 을 POST 합니다. 서명을 검증하고, 중복을 거르고, 2xx 로 빨리 답하면 됩니다.
등록
설정 > API > 웹훅에서 주소를 추가합니다. 모드(라이브 · 테스트)마다 엔드포인트 5개까지, 엔드포인트마다 받을 이벤트를 고릅니다(기본 전부). 입금과 출금을 다른 서버로 받고 싶으면 엔드포인트 둘에 이벤트를 나눠 고르면 됩니다.
https만 받습니다. 테스트 모드도 같습니다. 자체 서명 인증서는 안 됩니다- 리다이렉트는 따라가지 않습니다 — 3xx 는 실패입니다. 최종 주소를 등록하세요
localhost· 사설 IP 는 등록할 수 없습니다. 로컬 개발은 아래 "로컬에서 받기"- 시크릿(
whsec_…)은 등록할 때 만들어지고 화면에서 다시 볼 수 있습니다
봉투와 헤더
Standard Webhooks 규격 그대로입니다.
POST /your/endpoint HTTP/1.1
content-type: application/json
webhook-id: evt_8f3k2m9d1a2b3c4d5e6f7a8b9c0d1e2f
webhook-timestamp: 1791000000
webhook-signature: v1,K3h0uQ9…=
{
"id": "evt_8f3k2m9d1a2b3c4d5e6f7a8b9c0d1e2f",
"object": "event",
"type": "order.paid",
"livemode": true,
"created_at": "2026-10-06T14:11:52+09:00",
"data": { "object": { "id": "ord_…", "object": "order", "status": "paid", "…": "…" } }
}
webhook-id는 이벤트 id 와 같고 재시도해도 바뀌지 않습니다. 중복은 이것으로 거릅니다data.object는 이벤트가 난 순간의 스냅샷입니다. 지금 상태가 필요하면GET /v1/orders/{id}로 다시 읽습니다- 모르는
type과 모르는 필드는 무시하세요 — 더하는 변경은 버전을 올리지 않습니다
이벤트
| type | 언제 | data.object |
|---|---|---|
order.created | 주문이 생김 — API · 결제 링크 · 쇼핑몰 | order |
order.updated | PATCH /v1/orders/{id} | order |
order.paid | 결제 확정 — 자동 매칭 · 화면에서 확정 · match · mark-paid · 쇼핑몰 밖 결제 | order (payment 포함) |
order.canceled | 취소 | order |
order.expired | 매칭 마감이 지남 | order |
order.payment_reversed | 되돌리기 — 주문은 다시 open | order + data.previous_payment |
transaction.deposited | 입금 통지를 받고 첫 매칭 판정이 끝난 뒤 — 미매칭도 여기 | transaction (status 가 결과) |
transaction.withdrawn | 출금 통지 | transaction |
transaction.held | 입금이 보류가 됨 — 후보 여럿 · 늦게 온 주문 · 규칙 밖 이름 | transaction + data.candidate_order_ids |
한 입금에서 여러 이벤트가 나면 transaction.deposited → order.paid(또는 transaction.held) 순으로 기록됩니다. 그러나 도착 순서는 보장하지 않습니다 — 전송은 이벤트마다 독립입니다. 순서가 필요하면 GET /v1/events 의 목록 순서(기록 순)와 객체의 현재 상태를 보세요.
서명 검증
시크릿은 whsec_ + base64 입니다. 서명은 HMAC-SHA256(secret, "{webhook-id}.{webhook-timestamp}.{본문}") 의 base64 이고, 헤더에 v1, 을 붙여 옵니다. 라이브러리가 서명과 시각(5분)을 함께 확인합니다. 본문은 파싱하기 전의 원문 그대로 넘깁니다 — 다시 직렬화하면 바이트가 달라집니다.
Node
import { Webhook } from "standardwebhooks" // npm i standardwebhooks
const wh = new Webhook(secret) // "whsec_…" 그대로
const event = wh.verify(rawBody, {
"webhook-id": req.headers["webhook-id"],
"webhook-timestamp": req.headers["webhook-timestamp"],
"webhook-signature": req.headers["webhook-signature"],
})
Python
from standardwebhooks.webhooks import Webhook # pip install standardwebhooks
wh = Webhook(secret)
event = wh.verify(raw_body, headers) # headers: dict — webhook-id · webhook-timestamp · webhook-signature
Java
import com.standardwebhooks.Webhook;
Webhook webhook = new Webhook(secret);
webhook.verify(rawBody, headers); // 틀리면 WebhookVerificationException
PHP
use StandardWebhooks\Webhook;
$wh = new Webhook($secret);
$event = $wh->verify($rawBody, $headers); // 틀리면 예외
라이브러리 설치법과 다른 언어는 standard-webhooks 저장소에 있습니다.
시크릿을 재발급하면 24시간 동안 옛 · 새 시크릿 서명이 공백으로 이어져 둘 다 옵니다(v1,aaa v1,bbb). 위 라이브러리는 여러 서명 중 하나만 맞으면 통과시키므로 받는 서버의 시크릿을 그 사이에 바꾸면 끊기지 않습니다.
받는 쪽에서 지킬 것
- 2xx 로 10초 안에 답합니다. 그 밖(3xx 포함) · 타임아웃 · 연결 실패는 실패로 보고 재시도합니다. 무거운 처리는 답한 뒤에 하세요
- 중복을 거릅니다. 최소 한 번(at-least-once) 전달이라 같은
webhook-id가 두 번 올 수 있습니다. 처리한 id 를 기억해 두면 됩니다 - 순서를 믿지 않습니다.
order.paid뒤에transaction.deposited가 올 수 있습니다. 이벤트마다 따로 처리하도록 만드세요 - 응답 본문은 저장하지 않습니다. 짧게
{}로 충분합니다
재시도 · 실패 · 자동 끄기
| 시도 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|
| 간격 | 바로 | 1분 | 5분 | 30분 | 2시간 | 6시간 | 12시간 | 24시간 |
- 8번째도 실패하면 그 전송은
failed가 되고 조직 소유자에게 메일이 갑니다(엔드포인트마다 24시간에 한 통) - 실패만 72시간 이어지면 엔드포인트가 자동으로 꺼지고 메일이 갑니다. 고친 뒤 설정 > API > 웹훅에서 다시 켭니다 — 꺼진 동안 것은 자동으로 다시 가지 않고, 전송 로그에서 건별로 재전송합니다
- 전송 로그(엔드포인트마다, 30일)에 시도마다 상태 코드 · 걸린 시간이 남습니다.
evt_로 찾을 수 있습니다 - 빠진 이벤트는
GET /v1/events(30일)로 언제든 끌어올 수 있습니다
테스트 이벤트
설정 > API > 웹훅 > 엔드포인트의 "테스트 이벤트 보내기"는 type: "webhook.test"(id 는 evt_test_…)를 실제와 같은 서명으로 보냅니다. 연결과 서명 검증만 확인하는 용도입니다 — 실제 주문 처리로 흘리지 마세요. 전송 로그에는 남지 않습니다.
로컬에서 받기
- 받는 서버를 띄웁니다(위 Node 예시는 3000 포트)
- 터널을 엽니다 —
ngrok http 3000또는cloudflared tunnel --url http://localhost:3000 - 터널이 준
https://…주소를 테스트 탭의 웹훅에 등록합니다 - 시작하기 4단계의 가짜 입금을 넣고 받는지 봅니다
터널 주소가 바뀌면 엔드포인트의 주소를 고칩니다(시크릿은 그대로입니다). 받는 서버 없이 개발하려면 GET /v1/events 를 주기적으로 읽어도 됩니다 — 같은 봉투가 옵니다.