Skip to main content

웹훅 시스템

Multi-SaaS Kit의 실시간 이벤트 알림 시스템입니다.

개요

웹훅을 통해 시스템 이벤트를 실시간으로 외부 서비스에 전달할 수 있습니다.

항목설명
프로토콜HTTPS (TLS 1.2+)
형식JSON
인증HMAC-SHA256 서명
재시도최대 5회 (지수 백오프)

지원 이벤트

사용자 이벤트

이벤트설명트리거 시점
user.created사용자 생성회원가입, 관리자 생성
user.updated사용자 정보 수정프로필 수정, 권한 변경
user.deleted사용자 삭제Soft Delete 시

테넌트 이벤트

이벤트설명트리거 시점
tenant.created테넌트 생성새 테넌트 등록
tenant.updated테넌트 정보 수정설정 변경
tenant.suspended테넌트 정지결제 실패, 관리자 조치
tenant.activated테넌트 활성화정지 해제

구독 이벤트 (플러그인)

이벤트설명트리거 시점
subscription.created구독 생성첫 구독
subscription.renewed구독 갱신자동/수동 갱신
subscription.canceled구독 취소취소 요청
subscription.expired구독 만료기간 종료

웹훅 등록

POST /api/v1/webhooks

새 웹훅 엔드포인트 등록

Request

POST /api/v1/webhooks
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json
{
"url": "https://your-app.com/webhooks/multi-saas",
"events": ["user.created", "user.updated", "tenant.created"],
"secret": "your-webhook-secret",
"active": true,
"description": "메인 웹훅 엔드포인트"
}

Response

{
"success": true,
"data": {
"id": "wh_abc123",
"url": "https://your-app.com/webhooks/multi-saas",
"events": ["user.created", "user.updated", "tenant.created"],
"active": true,
"created_at": "2024-01-20T00:00:00Z"
}
}

페이로드 구조

기본 구조

모든 웹훅 페이로드는 동일한 기본 구조를 따릅니다:

{
"id": "evt_1234567890abcdef",
"type": "user.created",
"created_at": "2024-01-20T12:00:00Z",
"tenant_id": 1,
"api_version": "v1",
"data": {
// 이벤트별 데이터
}
}
필드타입설명
idstring이벤트 고유 ID
typestring이벤트 유형
created_atstring이벤트 발생 시간 (ISO 8601)
tenant_idinteger테넌트 ID
api_versionstringAPI 버전
dataobject이벤트 데이터

이벤트별 페이로드

user.created

{
"id": "evt_usr_123",
"type": "user.created",
"created_at": "2024-01-20T12:00:00Z",
"tenant_id": 1,
"api_version": "v1",
"data": {
"user": {
"id": 10,
"name": "홍길동",
"email": "hong@example.com",
"permission_level": 6,
"created_at": "2024-01-20T12:00:00Z"
}
}
}

tenant.created

{
"id": "evt_ten_456",
"type": "tenant.created",
"created_at": "2024-01-20T12:00:00Z",
"tenant_id": 5,
"api_version": "v1",
"data": {
"tenant": {
"id": 5,
"name": "New Company",
"slug": "new-company",
"plan": "starter",
"owner": {
"id": 15,
"email": "admin@new-company.com"
},
"created_at": "2024-01-20T12:00:00Z"
}
}
}

서명 검증

서명 헤더

모든 웹훅 요청에는 서명 헤더가 포함됩니다:

POST /your-webhook-endpoint HTTP/1.1
Content-Type: application/json
X-Webhook-Signature: sha256=5d0e...
X-Webhook-Timestamp: 1705752000
X-Webhook-Event-Id: evt_1234567890

서명 생성 방법

signature = HMAC-SHA256(
secret,
timestamp + "." + request_body
)

PHP 검증 예제

<?php

class WebhookController extends Controller
{
public function handle(Request $request)
{
$payload = $request->getContent();
$signature = $request->header('X-Webhook-Signature');
$timestamp = $request->header('X-Webhook-Timestamp');

// 타임스탬프 검증 (5분 이내)
if (abs(time() - (int)$timestamp) > 300) {
return response()->json(['error' => 'Invalid timestamp'], 400);
}

// 서명 검증
$expectedSignature = 'sha256=' . hash_hmac(
'sha256',
$timestamp . '.' . $payload,
config('services.multi-saas.webhook_secret')
);

if (!hash_equals($expectedSignature, $signature)) {
return response()->json(['error' => 'Invalid signature'], 401);
}

// 이벤트 처리
$event = json_decode($payload, true);
$this->processEvent($event);

return response()->json(['received' => true]);
}

private function processEvent(array $event)
{
match($event['type']) {
'user.created' => $this->handleUserCreated($event['data']),
'tenant.created' => $this->handleTenantCreated($event['data']),
default => null,
};
}
}

JavaScript (Node.js) 검증 예제

const crypto = require('crypto');

function verifyWebhook(payload, signature, timestamp, secret) {
// 타임스탬프 검증 (5분 이내)
const currentTime = Math.floor(Date.now() / 1000);
if (Math.abs(currentTime - parseInt(timestamp)) > 300) {
throw new Error('Invalid timestamp');
}

// 서명 생성 및 비교
const expectedSignature = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${payload}`)
.digest('hex');

if (!crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
)) {
throw new Error('Invalid signature');
}

return JSON.parse(payload);
}

재시도 정책

재시도 조건

HTTP 상태 코드재시도 여부
2xx❌ 성공
3xx❌ 재시도 안 함
4xx (429 제외)❌ 재시도 안 함
429✅ 재시도
5xx✅ 재시도
타임아웃✅ 재시도

재시도 간격 (지수 백오프)

시도대기 시간
1차즉시
2차30초
3차2분
4차10분
5차1시간
이후실패 처리

실패 알림

5회 재시도 실패 시:

  • 웹훅 비활성화 (active: false)
  • 관리자 이메일 알림
  • 대시보드에 경고 표시

웹훅 관리

GET /api/v1/webhooks

등록된 웹훅 목록 조회

GET /api/v1/webhooks
Authorization: Bearer YOUR_TOKEN

PUT /api/v1/webhooks/:id

웹훅 설정 수정

PUT /api/v1/webhooks/wh_abc123
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json
{
"events": ["user.created", "user.updated"],
"active": true
}

DELETE /api/v1/webhooks/:id

웹훅 삭제

DELETE /api/v1/webhooks/wh_abc123
Authorization: Bearer YOUR_TOKEN

POST /api/v1/webhooks/:id/test

테스트 이벤트 전송

POST /api/v1/webhooks/wh_abc123/test
Authorization: Bearer YOUR_TOKEN

웹훅 로그

GET /api/v1/webhooks/:id/logs

웹훅 전송 로그 조회

GET /api/v1/webhooks/wh_abc123/logs?page=1
Authorization: Bearer YOUR_TOKEN
{
"success": true,
"data": [
{
"id": "log_123",
"event_id": "evt_usr_123",
"event_type": "user.created",
"status": "success",
"response_code": 200,
"response_time_ms": 150,
"attempts": 1,
"created_at": "2024-01-20T12:00:00Z"
},
{
"id": "log_124",
"event_id": "evt_ten_456",
"event_type": "tenant.created",
"status": "failed",
"response_code": 500,
"response_time_ms": 30000,
"attempts": 5,
"last_error": "Connection timeout",
"created_at": "2024-01-20T12:05:00Z"
}
]
}

베스트 프랙티스

1. 빠른 응답

// 좋음: 즉시 응답 후 비동기 처리
public function handle(Request $request)
{
// 서명 검증
$this->verifySignature($request);

// 큐에 작업 추가
ProcessWebhookJob::dispatch($request->all());

// 즉시 200 응답
return response()->json(['received' => true]);
}

2. 멱등성 보장

// 이벤트 ID로 중복 처리 방지
public function processEvent($event)
{
$eventId = $event['id'];

if (WebhookLog::where('event_id', $eventId)->exists()) {
return; // 이미 처리됨
}

// 처리 로직...

WebhookLog::create(['event_id' => $eventId, ...]);
}

3. 오류 처리

  • 타임아웃: 30초 이내 응답
  • 재시도 고려: 멱등성 보장
  • 로깅: 모든 웹훅 요청 기록

관련 문서