이벤트 유형 레퍼런스
이 레퍼런스는 Brevo에서 제공하는 모든 웹훅 이벤트 유형을 다루며, Tajo 로열티 플랫폼 연동에 특화된 예제를 함께 제공합니다.
이메일 이벤트
delivered
이메일이 수신자의 메일함으로 정상 전달되었을 때 발생합니다.
페이로드 예시:
{ "event": "delivered", "id": 123456, "date": "2024-01-25 14:30:00", "ts": 1640995200, "template_id": 101, "tags": ["loyalty", "points-earned"], "sending_ip": "185.107.232.1", "event_id": "evt_abc123"}Tajo 연동 활용 사례:
- 고객 프로필에 기록된 이메일 전달 성공률을 업데이트합니다
- 로열티 캠페인과 연결된 후속 액션을 자동으로 실행합니다
- 로열티 등급별로 이메일 전달 성과가 어떻게 다른지 추적합니다
async function handleEmailDelivered(event) { const customer = await loyaltyService.getCustomer(event.email);
// Update delivery stats await loyaltyService.updateEngagement(event.email, { emailsDelivered: customer.emailsDelivered + 1, lastEmailDelivered: new Date(event.date), deliveryRate: calculateDeliveryRate(customer) });
// Track loyalty campaign delivery if (event.tags.includes('loyalty')) { await loyaltyService.trackCampaignMetric('loyalty_email_delivered', { email: event.email, template_id: event.template_id, tier: customer.loyaltyTier }); }}opened
수신자가 이메일을 열었을 때 발생합니다(추적 픽셀 로딩 기준).
페이로드 예시:
{ "event": "opened", "date": "2024-01-25 15:45:00", "ts": 1641000300, "template_id": 101, "tags": ["loyalty", "tier-upgrade"], "user-agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)", "geo": { "country": "US", "region": "CA", "city": "San Francisco" }}Tajo 연동 활용 사례:
- 고객의 인게이지먼트 점수를 높여 참여도를 반영합니다
- 로열티 캠페인이 실제로 얼마나 효과적이었는지 추적합니다
- 열람 행동을 기준으로 리워드를 자동으로 지급합니다
- 이후 발송할 커뮤니케이션을 개인화하는 데 활용합니다
async function handleEmailOpened(event) { const customer = await loyaltyService.getCustomer(event.email);
// Significant engagement - boost score await loyaltyService.updateEngagement(event.email, { emailsOpened: customer.emailsOpened + 1, lastEmailOpened: new Date(event.date), engagementScore: customer.engagementScore + 5, preferredDevice: getDeviceType(event['user-agent']) });
// Reward engagement for loyalty members if (event.tags.includes('loyalty') && customer.loyaltyTier) { await loyaltyService.awardEngagementBonus(event.email, { type: 'email_engagement', points: getEngagementPoints(customer.loyaltyTier), reason: 'Email opened' }); }
// Track timing patterns for optimization await loyaltyService.recordEngagementTime(event.email, { campaign: event.template_id, openTime: new Date(event.date), timezone: getTimezone(event.geo) });}clicked
수신자가 이메일 안의 링크를 클릭했을 때 발생합니다.
페이로드 예시:
{ "event": "clicked", "date": "2024-01-25 16:20:00", "ts": 1641002400, "template_id": 101, "tags": ["loyalty", "rewards-reminder"], "link": "https://yourdomain.com/rewards?utm_source=brevo&utm_campaign=loyalty", "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}Tajo 연동 활용 사례:
- 전환 퍼널의 각 단계별 성과가 어떻게 달라지는지 추적합니다
- 링크를 클릭한 고객에게 인게이지먼트 보너스를 지급합니다
- 반응이 좋은 고가치 콘텐츠가 무엇인지 식별합니다
- 이메일에서 웹사이트로 이어지는 흐름을 최적화합니다
async function handleEmailClicked(event) { const customer = await loyaltyService.getCustomer(event.email); const clickedUrl = new URL(event.link);
// High-value engagement await loyaltyService.updateEngagement(event.email, { emailsClicked: customer.emailsClicked + 1, lastEmailClicked: new Date(event.date), engagementScore: customer.engagementScore + 15, clickThroughRate: calculateCTR(customer) });
// Track specific link types if (clickedUrl.pathname.includes('/rewards')) { await loyaltyService.trackEvent(event.email, 'Rewards Page Clicked', { source: 'email', campaign: event.template_id, utm_campaign: clickedUrl.searchParams.get('utm_campaign') });
// Award exploration bonus await loyaltyService.awardEngagementBonus(event.email, { type: 'rewards_exploration', points: 10, reason: 'Clicked rewards link' }); }
// Track product interest if (clickedUrl.pathname.includes('/products')) { const productId = extractProductId(clickedUrl); await loyaltyService.trackProductInterest(event.email, productId, { source: 'email_click', timestamp: new Date(event.date) }); }}bounced / hard_bounced
이메일이 반송되었을 때(일시적 실패) 또는 하드 바운스가 발생했을 때(영구적 실패) 발생합니다.
페이로드 예시:
{ "event": "hard_bounced", "date": "2024-01-25 14:35:00", "ts": 1640995500, "template_id": 101, "tags": ["loyalty"], "reason": "550 5.1.1 User unknown", "bounce_type": "hard"}Tajo 연동 활용 사례:
- 해당 이메일 주소의 유효성 상태를 즉시 업데이트합니다
- SMS 등 사용 가능한 대체 커뮤니케이션 채널로 전환합니다
- 고객 데이터베이스에서 잘못된 주소를 정리합니다
- 유효하지 않은 주소로 추가 발송이 나가지 않도록 차단합니다
async function handleEmailBounced(event) { const isHardBounce = event.event === 'hard_bounced' || event.bounce_type === 'hard';
if (isHardBounce) { // Permanent failure - mark email as invalid await loyaltyService.updateCustomerStatus(event.email, { emailStatus: 'invalid', emailBounced: true, bounceReason: event.reason, lastBounce: new Date(event.date), communicationPreference: 'sms' // Switch to SMS if available });
// Remove from email marketing lists await loyaltyService.removeFromEmailMarketing(event.email);
// Suggest phone verification const customer = await loyaltyService.getCustomer(event.email); if (customer?.phone) { await loyaltyService.suggestPhoneVerification(customer.phone); }
} else { // Soft bounce - temporary issue await loyaltyService.updateEngagement(event.email, { softBounceCount: customer.softBounceCount + 1, lastSoftBounce: new Date(event.date) });
// Retry logic for soft bounces if (customer.softBounceCount < 5) { await loyaltyService.scheduleEmailRetry(event.email, event.template_id); } }}spam
수신자가 이메일을 스팸으로 신고했을 때 발생합니다.
페이로드 예시:
{ "event": "spam", "date": "2024-01-25 17:10:00", "ts": 1641005400, "template_id": 101, "tags": ["loyalty", "promotional"]}Tajo 연동 활용 사례:
- 해당 고객에 대한 이메일 발송을 즉시 중단합니다
- 이메일 콘텐츠를 검토하고 개선할 지점을 찾습니다
- 세그먼트별로 반복되는 스팸 신고 패턴이 있는지 분석합니다
- 더 엄격한 옵트인 절차를 도입하는 근거로 삼습니다
async function handleEmailSpam(event) { // Immediately disable email marketing await loyaltyService.updateCustomerPreferences(event.email, { emailMarketing: false, marketingEnabled: false, spamReported: true, spamReportDate: new Date(event.date) });
// Alert marketing team for review await loyaltyService.alertMarketing('spam_complaint', { email: event.email, template: event.template_id, campaign_tags: event.tags, severity: 'high' });
// Analyze spam patterns await loyaltyService.analyzeSpamPattern({ template_id: event.template_id, tags: event.tags, customer_segment: await loyaltyService.getCustomerSegment(event.email) });
// Consider account review if multiple spam reports const customer = await loyaltyService.getCustomer(event.email); if (customer.spamReports > 2) { await loyaltyService.flagForReview(event.email, 'multiple_spam_reports'); }}SMS 이벤트
sms_delivered
SMS가 수신자의 휴대전화로 정상 전달되었을 때 발생합니다.
페이로드 예시:
{ "event": "sms_delivered", "phone": "+1234567890", "date": "2024-01-25 14:45:00", "ts": 1640996700, "message-id": "sms_abc123", "tags": ["loyalty", "points-alert"], "sender": "TAJO", "content": "🎉 Great news! You earned 150 points from your recent purchase. Total: 1,250 points."}Tajo 연동 활용 사례:
- SMS가 수신자에게 정상적으로 전달되었는지 확인합니다
- 채널별로 SMS 인게이지먼트율이 어떻게 변하는지 추적합니다
- 등록된 전화번호가 정확한지 검증하는 근거로 활용합니다
- 통신사별 전달 성과를 지속적으로 모니터링합니다
async function handleSMSDelivered(event) { const customer = await loyaltyService.getCustomerByPhone(event.phone);
await loyaltyService.updateEngagement(customer.email, { smsDelivered: customer.smsDelivered + 1, lastSMSDelivered: new Date(event.date), smsDeliveryRate: calculateSMSDeliveryRate(customer), phoneStatus: 'valid' });
// Track loyalty SMS performance if (event.tags.includes('loyalty')) { await loyaltyService.trackCampaignMetric('loyalty_sms_delivered', { phone: event.phone, content_type: getSMSContentType(event.content), customer_tier: customer.loyaltyTier }); }}sms_failed
SMS 전달에 실패했을 때 발생합니다.
페이로드 예시:
{ "event": "sms_failed", "phone": "+1234567890", "date": "2024-01-25 14:32:00", "ts": 1640996320, "message-id": "sms_def456", "tags": ["loyalty", "urgent"], "sender": "TAJO", "reason": "Invalid phone number format", "error_code": "30006"}Tajo 연동 활용 사례:
- 해당 전화번호의 유효성 상태를 업데이트합니다
- 대체 수단으로 이메일 알림 발송으로 전환합니다
- 전화번호 데이터베이스에서 잘못된 번호를 정리합니다
- 수동 확인이 이루어지도록 고객 서비스팀에 알립니다
async function handleSMSFailed(event) { const customer = await loyaltyService.getCustomerByPhone(event.phone);
await loyaltyService.updateCustomerStatus(customer.email, { phoneStatus: 'invalid', smsEnabled: false, smsFailureReason: event.reason, lastSMSFailure: new Date(event.date), communicationPreference: 'email' });
// For urgent loyalty notifications, fall back to email if (event.tags.includes('urgent') || event.tags.includes('loyalty')) { await loyaltyService.sendEmailFallback(customer.email, { originalSMS: event.content, reason: 'SMS delivery failed' }); }
// Flag for phone number verification await loyaltyService.flagForPhoneVerification(customer.email);}sms_reply
수신자가 SMS에 답장했을 때 발생합니다.
페이로드 예시:
{ "event": "sms_reply", "phone": "+1234567890", "date": "2024-01-25 15:20:00", "ts": 1641000000, "message-id": "sms_reply_123", "text": "BALANCE", "original_message_id": "sms_abc123"}Tajo 연동 활용 사례:
- BALANCE, REWARDS 같은 로열티 프로그램 명령어를 처리합니다
- 답장으로 들어온 고객 서비스 요청을 담당자에게 전달합니다
- 고객의 커뮤니케이션 선호 설정을 업데이트합니다
- 명령어 내용에 맞는 자동 응답 메시지를 즉시 발송합니다
async function handleSMSReply(event) { const customer = await loyaltyService.getCustomerByPhone(event.phone); const replyText = event.text.toUpperCase().trim();
// High engagement - customer actively participating await loyaltyService.updateEngagement(customer.email, { smsReplies: customer.smsReplies + 1, lastSMSReply: new Date(event.date), engagementScore: customer.engagementScore + 10 });
// Process loyalty commands switch (replyText) { case 'BALANCE': await loyaltyService.sendPointsBalance(event.phone); break;
case 'REWARDS': await loyaltyService.sendAvailableRewards(event.phone, customer.loyaltyTier); break;
case 'TIER': await loyaltyService.sendTierInfo(event.phone, customer); break;
case 'HELP': await loyaltyService.sendSMSHelp(event.phone); break;
case 'STOP': case 'UNSUBSCRIBE': await loyaltyService.unsubscribeFromSMS(event.phone); break;
default: // Forward to customer service await loyaltyService.forwardToSupport(customer.email, { channel: 'sms', message: event.text, timestamp: new Date(event.date) }); }}연락처 이벤트
contact_created
새 연락처가 Brevo에 추가되었을 때 발생합니다.
페이로드 예시:
{ "event": "contact_created", "date": "2024-01-25 13:15:00", "ts": 1640992500, "attributes": { "FIRSTNAME": "John", "LASTNAME": "Doe", "LOYALTY_ID": "LYL-2024-001", "LOYALTY_TIER": "Bronze", "LOYALTY_POINTS": 0 }, "lists": [1, 5]}Tajo 연동 활용 사례:
- 신규 연락처를 대상으로 웰컴 캠페인을 실행합니다
- 로열티 프로그램 가입 절차를 자동으로 처리합니다
- 고객 여정을 처음 상태로 초기화하고 온보딩을 시작합니다
- 가입 보너스 포인트를 지급해 첫 참여를 자연스럽게 유도합니다
async function handleContactCreated(event) { const isLoyaltyMember = event.attributes?.LOYALTY_ID;
if (isLoyaltyMember) { // New loyalty member - trigger welcome flow await loyaltyService.triggerWelcomeFlow(event.email, { loyaltyId: event.attributes.LOYALTY_ID, tier: event.attributes.LOYALTY_TIER || 'Bronze', signupBonus: 500 });
// Award signup bonus await loyaltyService.awardPoints(event.email, { amount: 500, reason: 'Welcome bonus', type: 'signup_bonus' });
// Schedule onboarding emails await loyaltyService.scheduleOnboardingSequence(event.email, { tier: event.attributes.LOYALTY_TIER, preferences: event.attributes }); }
// Track signup source await loyaltyService.trackSignupSource(event.email, { lists: event.lists, attributes: event.attributes, timestamp: new Date(event.date) });}contact_updated
연락처 정보가 업데이트되었을 때 발생합니다.
페이로드 예시:
{ "event": "contact_updated", "date": "2024-01-25 16:45:00", "ts": 1641003900, "updated_attributes": { "LOYALTY_POINTS": 1250, "LOYALTY_TIER": "Silver", "TOTAL_SPENT": 899.99 }, "previous_attributes": { "LOYALTY_POINTS": 750, "LOYALTY_TIER": "Bronze", "TOTAL_SPENT": 549.99 }}Tajo 연동 활용 사례:
- 로열티 등급이 상승했는지 자동으로 감지합니다
- 이전 값과 비교해 포인트 잔액이 어떻게 변했는지 추적합니다
- 고객 프로필의 완성도를 지속적으로 모니터링합니다
- 새로운 등급에 맞는 맞춤 캠페인을 이어서 실행합니다
async function handleContactUpdated(event) { const updated = event.updated_attributes; const previous = event.previous_attributes;
// Check for tier upgrade if (updated.LOYALTY_TIER && updated.LOYALTY_TIER !== previous.LOYALTY_TIER) { await loyaltyService.handleTierUpgrade(event.email, { previousTier: previous.LOYALTY_TIER, newTier: updated.LOYALTY_TIER, pointsBalance: updated.LOYALTY_POINTS });
// Send congratulations await loyaltyService.sendTierUpgradeEmail(event.email, { newTier: updated.LOYALTY_TIER, benefits: loyaltyService.getTierBenefits(updated.LOYALTY_TIER) }); }
// Track significant point increases const pointIncrease = updated.LOYALTY_POINTS - previous.LOYALTY_POINTS; if (pointIncrease > 0) { await loyaltyService.trackPointsEarned(event.email, { amount: pointIncrease, newTotal: updated.LOYALTY_POINTS, source: 'profile_update' }); }
// Monitor spending milestones if (updated.TOTAL_SPENT > previous.TOTAL_SPENT) { await loyaltyService.checkSpendingMilestones(event.email, { currentSpend: updated.TOTAL_SPENT, previousSpend: previous.TOTAL_SPENT }); }}이벤트 처리 모범 사례
1. 멱등성 처리
class WebhookProcessor { constructor() { this.processedEvents = new Set(); }
async processEvent(event) { const eventKey = `${event.event}_${event.email}_${event.ts}`;
if (this.processedEvents.has(eventKey)) { console.log('Duplicate event ignored:', eventKey); return; }
this.processedEvents.add(eventKey);
try { await this.handleEvent(event); } catch (error) { this.processedEvents.delete(eventKey); // Allow retry throw error; } }}2. 이벤트 순서 보장
async function processEventInSequence(event) { const customer = await loyaltyService.getCustomer(event.email); const lastProcessedTime = customer.lastWebhookProcessed || 0;
// Ensure events are processed in chronological order if (event.ts < lastProcessedTime) { console.warn('Out-of-order event received, queuing for later processing'); await loyaltyService.queueEventForLaterProcessing(event); return; }
await handleWebhookEvent(event);
// Update last processed timestamp await loyaltyService.updateCustomer(event.email, { lastWebhookProcessed: event.ts });}3. 배치 처리
class BatchEventProcessor { constructor() { this.eventBatch = []; this.batchSize = 100; this.flushInterval = 5000; // 5 seconds
setInterval(() => this.flushBatch(), this.flushInterval); }
addEvent(event) { this.eventBatch.push(event);
if (this.eventBatch.length >= this.batchSize) { this.flushBatch(); } }
async flushBatch() { if (this.eventBatch.length === 0) return;
const batch = this.eventBatch.splice(0);
try { await loyaltyService.processBatchEvents(batch); } catch (error) { console.error('Batch processing failed:', error); // Re-queue failed events this.eventBatch.unshift(...batch); } }}