API 레퍼런스: 웹훅
이벤트가 생기면 웹훅으로 내 서버에 알려 줘요. 더보기 → Merchant API에서 앱의 **웹훅 URL (선택)**을 지정해 두면, 구독한 이벤트마다 서명된 JSON 본문을 POST해 드려요.
이벤트 형식
{
"update_id": 123,
"update_type": "invoice_paid",
"request_date": "2026-08-11T12:00:00Z",
"payload": { … }
}
update_id는 같은 이벤트를 다시 보낼 때도 그대로예요. 중복 제거는 이 값으로
하세요. request_date는 보낼 때마다 새로 찍혀요. payload에는 이벤트 종류에
맞는 객체가 통째로 들어가요. 청구서 이벤트면 청구서 객체, 송금 링크 객체, 송금 객체, 구독
객체 같은 식이에요(각각의 모양은 해당 레퍼런스에
있어요).
이벤트 종류
update_type | 언제 오나요 | 페이로드 |
|---|---|---|
invoice_paid | 청구서가 결제됐을 때 — 별도 구독 설정이 필요 없어요 | 청구서 객체 |
invoice_expired | 결제되지 않은 채 기한이 지났을 때 | 청구서 객체 |
check_activated | 송금 링크 전체나 명단의 한 사람이 자기 몫을 받았을 때 | 송금 링크 객체. 명단형 수령에는 activated_slot도 포함돼요 |
check_expired | 명단형 송금 링크가 만료되어 미수령 금액이 앱으로 돌아왔을 때 | recipients와 각 항목의 claimed_at을 포함한 송금 링크 객체 |
transfer_completed | 송금이 완료되거나 온체인 지급이 확인됐을 때 | 송금 또는 출금 객체 |
transfer_returned | 미수령 송금이 앱으로 반환되거나 온체인 지급이 실패해 보류액이 반환됐을 때 | 송금 또는 출금 객체 |
refund_completed | 환불이 나갔을 때 | refunded_* 필드가 채워진 청구서 객체 |
subscription_activated | 사용자가 요금제를 승인했을 때 | 구독 객체 |
subscription_charged | 주기가 청구됐을 때 — 어떤 청구인지는 charge.kind에 있어요: initial, renewal, resubscribe | 구독 객체 + charge: {period_no, kind, asset, amount, fee, paid_at} |
subscription_cancelled | 어느 쪽이든 구독을 취소했을 때 | 구독 객체 |
subscription_expired | 유예 기간이 결제 없이 끝났을 때 | 구독 객체 |
앱에서 만든 온체인 지급도 같은 송금 이벤트를 사용해요. 페이로드에는 kind: "withdrawal", network, address, txid 같은 체인 필드와 completed 또는 failed 상태가 있어요. transfer_id는 별도의 ID 순번을 사용하므로 사용 전에 kind로 구분하세요. 다운로드용 OpenAPI 레퍼런스는 두 페이로드 형식을 설명해요.
invoice_paid 말고는 전부 원할 때만 켜는 방식이에요(Crypto Bot에는 없는
확장 기능이라, Crypto Bot 규격만 아는 클라이언트는 직접 켜지 않는 한 모르는
update_type을 만날 일이 없어요). 앱마다 Merchant API 화면의 추가 웹훅
이벤트에서 켜면 돼요. 웹훅 섹션의 스위치에는 위 표의 식별자가 표시돼요. 이벤트를 받으려면 웹훅 URL을 저장하세요.
서명 확인하기
전송마다 TgPayCrypto-API-Signature 헤더가 붙어요(TgCryptoPay-API-Signature와 Crypto-Pay-API-Signature도 값이 같은 호환용
별칭이에요). 원본 요청 본문을 HMAC-SHA256으로 서명한 16진수 값이고, 키는 앱의
기본 API 토큰을 SHA-256으로 해시한 값이에요. Crypto Bot과 같은 방식이라
쓰던 검증 코드를 그대로 써도 돼요.
import hashlib, hmac
secret = hashlib.sha256(API_TOKEN.encode()).digest()
expected = hmac.new(secret, raw_body, hashlib.sha256).hexdigest()
ok = hmac.compare_digest(expected, headers["TgPayCrypto-API-Signature"])
받은 원본 바이트로 확인해야 해요. 다시 직렬화한 값은 바이트가 달라질 수 있어서 검증이 실패해요. 서명하는 건 기본 토큰뿐이고, 권한 제한 토큰은 서명하지 않아요. 기본 토큰을 새로 발급하면 웹훅 서명 키도 그 순간 바뀌니까, 서버의 시크릿도 같이 바꿔 주세요.
전송과 재시도
- 시도 제한 시간 안에 2xx 응답을 받으면 성공으로 처리해요. 빠르게 응답하고 오래 걸리는 작업은 별도로 처리하세요.
- 오류 상태, 시간 초과, 연결 실패가 발생하면 플랫폼의 시도 한도까지 간격을 늘려 재전송해요. 전달을 보장하지는 않으므로 장애 후에는
getInvoices로 결제 내역을 대조하세요. - 마지막 시도까지 실패하면 그 전송은 버려요. 웹훅 주소 자체가 자동으로 꺼지지는 않아요. 엔드포인트가 불안정하다고 해서 앱의 이벤트 구독이 자동으로 해제되지는 않아요.
- 다시 보내는 일이 있으니 핸들러는 같은 이벤트를 여러 번 받아도 중복 처리하지 않아야 해요. 처리하기
전에
update_id로 중복을 걸러 주세요.
⚠️ 처리하기 전에 확인하세요
내 웹훅 주소로는 누구나 POST를 보낼 수 있어요. 서명 확인이 통과하기
전까지는 본문을 믿지 마세요. 주문을 보내지도, 사용자 잔액에 반영하지도,
결제됨으로 표시하지도 마세요. 안전한 순서는 서명 확인 → update_id로 중복
제거 → 처리예요.
이 문서가 도움이 됐나요?
의견 고마워요.