--- id: wiki-2026-0508-webhooks-and-notifications title: WebHooks and Notifications category: 10_Wiki/Topics status: verified canonical_id: self aliases: [Webhooks, Event Callbacks, Push Notifications] duplicate_of: none source_trust_level: A confidence_score: 0.9 verification_status: applied tags: [webhooks, event-driven, http, async, integration] raw_sources: [] last_reinforced: 2026-05-10 github_commit: pending tech_stack: language: TypeScript / Python framework: Hono / FastAPI / Svix --- # WebHooks and Notifications ## 매 한 줄 > **"매 reverse-API: 매 server 가 client 의 HTTP endpoint 로 event 를 push 하는 매 async integration pattern"**. 2007 GitHub 가 popularize, 2026 Stripe/Shopify/Slack 의 표준. 매 polling 대비 latency ↓ 99%, 매 challenge: delivery guarantee + signature verification + replay protection. ## 매 핵심 ### 매 Webhook anatomy - `POST https://your-app/hooks/stripe` with JSON body + headers (`Stripe-Signature`, `Webhook-Id`, `Webhook-Timestamp`). - 매 receiver 의 2xx 응답 < 매 5s — 매 그렇지 않으면 sender 가 retry. ### 매 Delivery guarantees - **At-least-once**: 매 표준. 매 idempotency key 필수. - 매 retry: exponential backoff (1m, 5m, 30m, 2h, ...) up to 매 24-72h. - 매 dead-letter queue + manual replay UI. ### 매 Security 1. HMAC signature (`HMAC-SHA256(secret, timestamp + body)`). 2. Timestamp tolerance (±5 min) → replay 방어. 3. HTTPS only, IP allowlist (optional). 4. 매 secret rotation 의 지원. ### 매 응용 1. Payment events (Stripe, Toss). 2. SCM events (GitHub push, PR). 3. Chat platform commands (Slack, Discord). 4. 매 SaaS integration hub (Zapier, n8n). ## 💻 패턴 ### Receiver (Hono + signature verify) ```typescript import { Hono } from 'hono'; import { createHmac, timingSafeEqual } from 'crypto'; const app = new Hono(); app.post('/hooks/stripe', async (c) => { const sig = c.req.header('stripe-signature')!; const body = await c.req.text(); const [t, v1] = sig.split(',').map(p => p.split('=')[1]); if (Math.abs(Date.now()/1000 - +t) > 300) return c.text('stale', 400); const expected = createHmac('sha256', process.env.STRIPE_SECRET!) .update(`${t}.${body}`).digest('hex'); if (!timingSafeEqual(Buffer.from(expected), Buffer.from(v1))) return c.text('bad sig', 401); const event = JSON.parse(body); await enqueue(event); // 매 fast 200, async process return c.text('ok', 200); }); ``` ### Sender (with retry queue) ```python from svix import Svix svix = Svix("sk_...") svix.message.create("app_xxx", { "event_type": "user.created", "payload": {"id": user.id, "email": user.email}, }) # Svix handles signing, retries, replay log ``` ### Idempotency ```typescript async function handle(event: Event) { const exists = await redis.set( `evt:${event.id}`, '1', { NX: true, EX: 86400 } ); if (!exists) return; // 매 already processed await processEvent(event); } ``` ### Retry policy ```typescript const RETRY_SCHEDULE = [60, 300, 1800, 7200, 21600, 86400]; // seconds async function deliver(hook, attempt = 0) { try { const r = await fetch(hook.url, { method: 'POST', body: hook.body, headers: hook.headers }); if (r.ok) return; throw new Error(`status ${r.status}`); } catch (e) { if (attempt >= RETRY_SCHEDULE.length) { await deadLetter(hook); return; } await scheduleAt(deliver, hook, attempt + 1, Date.now() + RETRY_SCHEDULE[attempt] * 1000); } } ``` ### Push notification (FCM HTTP v1) ```typescript await fetch(`https://fcm.googleapis.com/v1/projects/${PROJECT}/messages:send`, { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ message: { token: deviceToken, notification: { title: 'New message', body: msg.preview }, data: { conversationId: msg.conversationId }, android: { priority: 'HIGH' }, apns: { payload: { aps: { 'mutable-content': 1 } } }, }, }), }); ``` ### Webhook → SQS bridge (decouple) ```typescript app.post('/hooks/:provider', async c => { const body = await c.req.text(); await sqs.send(new SendMessageCommand({ QueueUrl: process.env.HOOKS_QUEUE, MessageBody: JSON.stringify({ provider: c.req.param('provider'), body, hdr: c.req.header() }), })); return c.text('ok', 200); // 매 fast ack }); ``` ## 매 결정 기준 | 상황 | Approach | |---|---| | 매 outbound webhooks | Svix / Hookdeck (managed) | | 매 high-volume inbound | bridge to SQS/Kafka, process async | | Mobile push | FCM (Android+iOS) / APNs direct | | Web push | VAPID + Service Worker | | Internal pub/sub | NATS, Redis Streams (not webhooks) | **기본값**: HMAC-SHA256 signature + idempotency + async queue + 6-step retry + dead-letter. ## 🔗 Graph - 부모: [[Event-Driven-Architecture]] · [[HTTP-API]] - 변형: [[WebSockets_and_Realtime]] · [[Server-Sent-Events]] - 응용: [[Slack-Bot-Development]] - Adjacent: [[Idempotency]] ## 🤖 LLM 활용 **언제**: webhook receiver scaffold, signature-verify code, retry policy boilerplate. **언제 X**: secret 관리, production replay tool 설계 — domain expertise 필요. ## ❌ 안티패턴 - **No signature verification**: 매 anyone 의 spoof 가능. - **Sync heavy work in handler**: 매 timeout → sender retry storm. - **No idempotency**: at-least-once 의 duplicate 처리 → double-charge. - **Storing secret in code**: 매 secret rotation 불가. - **No dead-letter visibility**: silent failure. ## 🧪 검증 / 중복 - Verified (Stripe/GitHub webhook docs, Svix docs, Standard Webhooks spec 2024). - 신뢰도 A. ## 🕓 Changelog | 날짜 | 변경 | |---|---| | 2026-05-08 | Phase 1 | | 2026-05-10 | Manual cleanup — webhook delivery + signature + push 패턴 |