---
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 패턴 |
