웹훅
웹훅은 알림의 전달 상태가 바뀔 때 Posmit이 고객사 서버로 이벤트를 push 하는 기능입니다. 발송 결과를 폴링하지 않아도 전달 완료·읽음·실패·취소를 실시간에 가깝게 받아볼 수 있습니다.
등록한 URL로 JSON payload가 POST 되고, 모든 요청에는 HMAC-SHA256 서명이 포함되어 위조 요청을 걸러낼 수 있습니다.
이벤트 타입
이벤트는 아래 4종입니다.
| 이벤트 | 발생 시점 |
|---|---|
notification.delivered | 알림이 수신자에게 전달 완료됨 (공급자 콜백 또는 상태 폴링으로 확인) |
notification.read | 수신자가 알림을 읽음 (읽음 확인을 지원하는 채널) |
notification.failed | 발송이 최종 실패함 (재시도 불가 또는 재시도 소진) |
notification.canceled | 알림이 취소됨 (API 취소 또는 공급자 측 취소) |
Payload 구조
{
"eventId": "8c1f0b9e-4d2a-4c7b-9f1e-2a6d8e5c3b71",
"eventType": "notification.delivered",
"workspaceId": "11111111-2222-3333-4444-555555555555",
"notificationRequestId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"status": "delivered",
"occurredAt": "2026-07-10T12:00:00.000Z",
"payload": {
"provider": "solapi_sms",
"externalEventId": "MSG-20260710-0001"
}
}| 필드 | 타입 | 설명 |
|---|---|---|
eventId | string (UUID) | 전송 건마다 고유. 멱등 처리 키로 사용하세요 |
eventType | string | 위 4종 이벤트 타입 중 하나 |
workspaceId | string (UUID) | 워크스페이스 ID |
notificationRequestId | string (UUID) | 원본 알림 요청 ID |
status | string | 발행 시점의 전달 상태 문자열 |
occurredAt | string | 이벤트 발생 시각 (ISO 8601) |
payload | object | 이벤트별 부가 정보 (문자열 맵) |
payload 내부 필드는 이벤트에 따라 다릅니다.
| 이벤트 | payload 필드 |
|---|---|
notification.delivered | provider, externalEventId(있는 경우) |
notification.read | provider, externalEventId, readConfirmed: "true" |
notification.failed | provider(콜백 유래 시), errorCode, errorMessage, externalEventId(있는 경우) |
notification.canceled | 공급자 취소: provider, externalEventId / API 취소: reason: "canceled_by_api" |
요청 헤더와 서명
웹훅 요청은 아래 헤더와 함께 POST 됩니다.
| 헤더 | 값 |
|---|---|
Content-Type | application/json |
X-ProjectNoti-Event | 이벤트 타입 (예: notification.delivered) |
X-ProjectNoti-Timestamp | 서명 시각. Unix epoch 초 (문자열) |
X-ProjectNoti-Signature | HMAC-SHA256 서명의 hex 문자열 |
User-Agent | project-noti-webhook/1.0 |
서명은 다음과 같이 계산됩니다.
signature = HMAC-SHA256(secret, "{timestamp}.{rawBody}") → hex 인코딩secret— 구독 생성(또는 시크릿 회전) 시 딱 한 번 발급되는 64자 hex 문자열timestamp—X-ProjectNoti-Timestamp헤더 값rawBody— 수신한 원문 요청 바디 (파싱 전 바이트 그대로)
반드시 원문 바디로 검증하세요. JSON을 파싱했다가 다시 직렬화하면 공백이나 키 순서가 달라져 서명이 일치하지 않을 수 있습니다. 프레임워크의 raw body 옵션으로 원본 바이트를 보관한 뒤 검증해야 합니다.
서명 검증 예시 (TypeScript / Express)
import express from 'express';
import crypto from 'node:crypto';
const app = express();
const WEBHOOK_SECRET = process.env.POSMIT_WEBHOOK_SECRET!; // YOUR_HMAC_SECRET (64자 hex)
// 서명 검증을 위해 원문 바디를 그대로 받습니다
app.post(
'/webhooks/posmit',
express.raw({ type: 'application/json' }),
(req, res) => {
const timestamp = req.get('x-projectnoti-timestamp') ?? '';
const signature = req.get('x-projectnoti-signature') ?? '';
const rawBody = req.body as Buffer;
// 1. 서명 재계산: HMAC-SHA256(secret, "timestamp.rawBody")
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(`${timestamp}.${rawBody.toString('utf8')}`)
.digest('hex');
// 2. 상수 시간 비교 (타이밍 공격 방지)
const valid =
expected.length === signature.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
if (!valid) return res.sendStatus(401);
// 3. 리플레이 방지: timestamp 신선도 검사 (권장, 수신 측 책임)
const ageSeconds = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
if (!Number.isFinite(ageSeconds) || ageSeconds > 300) return res.sendStatus(401);
// 4. 검증 통과 후에만 파싱
const event = JSON.parse(rawBody.toString('utf8'));
// 5. eventId로 멱등 처리 (재시도로 같은 이벤트가 중복 도착할 수 있습니다)
// if (alreadyProcessed(event.eventId)) return res.sendStatus(200);
// 처리는 비동기로 넘기고 빠르게 2xx 응답
res.sendStatus(200);
handleEvent(event).catch(console.error);
},
);
async function handleEvent(event: { eventType: string; notificationRequestId: string }) {
switch (event.eventType) {
case 'notification.delivered':
// ...
break;
case 'notification.failed':
// ...
break;
}
}서명 timestamp는 재시도에도 최초 값이 그대로 유지됩니다. 신선도 검사 허용 범위를 정할 때 재시도 지연(기본 최대 약 3초 + 타임아웃)을 감안하되, 너무 좁게 잡지 않는 것을 권장합니다.
재시도 정책
| 항목 | 기본값 |
|---|---|
| 최대 시도 횟수 | 3회 |
| 백오프 | 지수 백오프 (1초 → 2초) |
| 요청 타임아웃 | 5초 |
| 성공 판정 | HTTP 2xx (200 이상 300 미만) |
- 2xx 외의 상태 코드, 네트워크 오류, 타임아웃은 모두 재시도 대상입니다.
- 모든 시도를 소진하면 해당 이벤트는 포기되고 로그만 남습니다. 별도의 지연 재전송 큐는 없으므로, 누락이 의심되면 알림 조회 API로 최종 상태를 확인하세요.
- 같은 이벤트는 조건에 맞는 모든 구독에 병렬로 전송됩니다.
- 웹훅 발행은 fire-and-forget 방식이라 발송 처리 흐름을 지연시키지 않습니다.
핸들러에서는 가능한 한 빠르게 2xx를 응답하세요. 처리에 5초 이상 걸리면 타임아웃으로
재시도가 발생해 같은 이벤트가 중복 도착합니다. 무거운 작업은 큐에 넣고 즉시 응답한 뒤,
eventId 기준으로 멱등하게 처리하는 것을 권장합니다.
구독 등록 (대시보드)
웹훅 구독은 대시보드에서만 등록·관리합니다. Notification API 키로는 구독을 생성할 수 없습니다.
엔드포인트 준비
https:// URL로 POST를 받을 수 있는 엔드포인트를 준비합니다. http / https 프로토콜만 허용됩니다.
구독 생성
대시보드의 웹훅 메뉴에서 이름, 대상 URL, 구독할 이벤트 타입을 입력해 구독을 생성합니다. 이벤트 타입은 1~10개를 선택할 수 있고, 생략하면 4종 전체를 구독합니다. 같은 워크스페이스에 동일한 URL은 중복 등록할 수 없습니다.
시크릿 보관
생성 응답에 서명 시크릿(32바이트 hex, 64자)이 딱 한 번 표시됩니다. 이후에는 다시 조회할 수 없으므로 즉시 안전한 곳(시크릿 매니저 등)에 보관하세요. 분실했다면 시크릿 회전으로 새 값을 발급받아야 합니다.
검증 코드 배포
위 서명 검증 예시를 참고해 수신 서버에 검증 로직을 배포하고, 실제 이벤트가 도착하는지 확인합니다.
구독 관리에서 할 수 있는 작업은 다음과 같습니다 (역할: owner / admin / operator, 조회는 viewer 포함).
| 작업 | 설명 |
|---|---|
| 목록 조회 | 등록된 구독과 상태 확인 |
| 생성 | 이름·URL·이벤트 타입 지정. 응답에서 시크릿 1회 노출 |
| 수정 | 이름·URL·이벤트 타입·상태(active / suspended) 변경 |
| 시크릿 회전 | 새 시크릿 발급 (응답에서 1회 노출, 기존 시크릿 즉시 무효) |
| 삭제 | 구독 제거 |
suspended 상태의 구독으로는 이벤트가 전송되지 않습니다.
시크릿을 회전하면 이전 시크릿은 즉시 무효화됩니다. 무중단 교체가 필요하면 새 시크릿을 수신 서버에 먼저 배포할 수 있도록 트래픽이 적은 시간대에 회전하는 것을 권장합니다.