Skip to main content

Webhook Event Types

Azotte provides comprehensive webhook events to keep your application synchronized with subscription, payment, and customer lifecycle changes.

Event Categories

Customer Events

Track customer lifecycle and profile changes:

// customer.created
{
"id": "evt_1234567890",
"object": "event",
"type": "customer.created",
"created": 1640995200,
"data": {
"object": {
"id": "cus_123abc",
"object": "customer",
"name": "John Doe",
"email": "john@example.com",
"created": 1640995200,
"metadata": {}
}
}
}

// customer.updated
{
"type": "customer.updated",
"data": {
"object": {
"id": "cus_123abc",
// updated customer object
},
"previous_attributes": {
"email": "old@example.com"
}
}
}

// customer.deleted
{
"type": "customer.deleted",
"data": {
"object": {
"id": "cus_123abc",
"object": "customer",
"deleted": true
}
}
}

Subscription Events

Monitor subscription state changes and lifecycle:

// subscription.created
{
"type": "subscription.created",
"data": {
"object": {
"id": "sub_456def",
"object": "subscription",
"customer": "cus_123abc",
"status": "active",
"current_period_start": 1640995200,
"current_period_end": 1643673600,
"plan": {
"id": "plan_basic",
"amount": 999,
"currency": "usd",
"interval": "month"
}
}
}
}

// subscription.updated
{
"type": "subscription.updated",
"data": {
"object": {
"id": "sub_456def",
"status": "past_due"
// updated subscription object
},
"previous_attributes": {
"status": "active"
}
}
}

// subscription.canceled
{
"type": "subscription.canceled",
"data": {
"object": {
"id": "sub_456def",
"status": "canceled",
"canceled_at": 1641081600,
"cancellation_reason": "customer_request"
}
}
}

// subscription.trial_will_end
{
"type": "subscription.trial_will_end",
"data": {
"object": {
"id": "sub_456def",
"trial_end": 1641168000, // 3 days from now
"status": "trialing"
}
}
}

Payment Events

Track payment processing and transaction status:

// payment.succeeded  
{
"type": "payment.succeeded",
"data": {
"object": {
"id": "pay_789ghi",
"object": "payment",
"amount": 999,
"currency": "usd",
"status": "succeeded",
"customer": "cus_123abc",
"payment_method": "pm_card_visa",
"created": 1640995200
}
}
}

// payment.failed
{
"type": "payment.failed",
"data": {
"object": {
"id": "pay_789ghi",
"status": "failed",
"failure_code": "card_declined",
"failure_message": "Your card was declined.",
"last_payment_error": {
"code": "card_declined",
"decline_code": "generic_decline",
"message": "Your card was declined."
}
}
}
}

// payment.refunded
{
"type": "payment.refunded",
"data": {
"object": {
"id": "pay_789ghi",
"amount_refunded": 999,
"refunds": {
"data": [
{
"id": "re_refund123",
"amount": 999,
"reason": "requested_by_customer",
"status": "succeeded"
}
]
}
}
}
}

Invoice Events

Monitor invoice generation and payment collection:

// invoice.created
{
"type": "invoice.created",
"data": {
"object": {
"id": "in_invoice123",
"object": "invoice",
"customer": "cus_123abc",
"subscription": "sub_456def",
"amount_due": 999,
"currency": "usd",
"status": "draft"
}
}
}

// invoice.payment_succeeded
{
"type": "invoice.payment_succeeded",
"data": {
"object": {
"id": "in_invoice123",
"status": "paid",
"amount_paid": 999,
"payment_intent": "pay_789ghi"
}
}
}

// invoice.payment_failed
{
"type": "invoice.payment_failed",
"data": {
"object": {
"id": "in_invoice123",
"status": "open",
"attempt_count": 1,
"next_payment_attempt": 1641082800
}
}
}

Dispute Events

Handle chargebacks and dispute lifecycle:

// dispute.created
{
"type": "dispute.created",
"data": {
"object": {
"id": "dp_dispute123",
"object": "dispute",
"amount": 999,
"currency": "usd",
"reason": "fraudulent",
"status": "warning_needs_response",
"payment": "pay_789ghi",
"evidence_due_by": 1641686400
}
}
}

// dispute.updated
{
"type": "dispute.updated",
"data": {
"object": {
"id": "dp_dispute123",
"status": "under_review"
},
"previous_attributes": {
"status": "needs_response"
}
}
}

Event Processing Patterns

Event Handler Structure

class WebhookEventProcessor {
async processEvent(event) {
switch (event.type) {
case 'customer.created':
return await this.handleCustomerCreated(event.data.object);

case 'subscription.created':
return await this.handleSubscriptionCreated(event.data.object);

case 'payment.succeeded':
return await this.handlePaymentSucceeded(event.data.object);

case 'payment.failed':
return await this.handlePaymentFailed(event.data.object);

case 'invoice.payment_failed':
return await this.handleInvoicePaymentFailed(event.data.object);

default:
console.log(`Unhandled event type: ${event.type}`);
return { processed: false };
}
}

async handleCustomerCreated(customer) {
// Send welcome email
await this.emailService.sendWelcomeEmail(customer.email);

// Create internal user record
await this.userService.createFromCustomer(customer);

// Track in analytics
await this.analytics.track('customer_created', {
customer_id: customer.id,
email: customer.email
});

return { processed: true };
}

async handlePaymentFailed(payment) {
// Log failed payment
await this.paymentService.logFailedPayment(payment);

// Send dunning email
await this.emailService.sendPaymentFailedEmail(
payment.customer,
payment.failure_message
);

// Update subscription status if needed
if (payment.invoice) {
await this.subscriptionService.handleFailedInvoicePayment(
payment.invoice
);
}

return { processed: true };
}
}

Batch Event Processing

import asyncio
from typing import List

class BatchEventProcessor:
def __init__(self, batch_size: int = 100):
self.batch_size = batch_size
self.event_queue = []

async def add_event(self, event: dict):
"""Add event to processing queue"""
self.event_queue.append(event)

if len(self.event_queue) >= self.batch_size:
await self.process_batch()

async def process_batch(self):
"""Process events in batches for efficiency"""
if not self.event_queue:
return

batch = self.event_queue[:self.batch_size]
self.event_queue = self.event_queue[self

# Group events by type for optimized processing
events_by_type = {}
for event in batch:
event_type = event['type']
if event_type not in events_by_type:
events_by_type[event_type] = []
events_by_type[event_type].append(event)

# Process each type in parallel
tasks = []
for event_type, events in events_by_type.items():
task = asyncio.create_task(
self.process_events_by_type(event_type, events)
)
tasks.append(task)

await asyncio.gather(*tasks)

async def process_events_by_type(self, event_type: str, events: List[dict]):
"""Optimize processing for specific event types"""
if event_type == 'payment.succeeded':
# Batch update revenue analytics
await self.update_revenue_batch(events)
elif event_type == 'customer.created':
# Batch send welcome emails
await self.send_welcome_emails_batch(events)
else:
# Process individually
for event in events:
await self.process_single_event(event)

Event-Driven Architecture

Event Sourcing Pattern

interface DomainEvent {
id: string;
type: string;
aggregateId: string;
aggregateVersion: number;
timestamp: Date;
data: any;
metadata?: any;
}

class CustomerAggregate {
private events: DomainEvent[] = [];
private version: number = 0;

static fromHistory(events: DomainEvent[]): CustomerAggregate {
const aggregate = new CustomerAggregate();

for (const event of events) {
aggregate.apply(event);
}

return aggregate;
}

createCustomer(customerId: string, name: string, email: string) {
const event: DomainEvent = {
id: generateId(),
type: 'CustomerCreated',
aggregateId: customerId,
aggregateVersion: this.version + 1,
timestamp: new Date(),
data: { customerId, name, email }
};

this.applyEvent(event);
}

updateCustomer(updates: Partial<Customer>) {
const event: DomainEvent = {
id: generateId(),
type: 'CustomerUpdated',
aggregateId: this.id,
aggregateVersion: this.version + 1,
timestamp: new Date(),
data: updates
};

this.applyEvent(event);
}

private applyEvent(event: DomainEvent) {
this.apply(event);
this.events.push(event);
}

getUncommittedEvents(): DomainEvent[] {
return this.events;
}
}

CQRS with Event Handlers

// Command side - write operations
public class SubscriptionCommandHandler
{
private readonly IEventStore _eventStore;

public async Task<string> Handle(CreateSubscriptionCommand command)
{
var subscription = new SubscriptionAggregate();
subscription.Create(
command.CustomerId,
command.PlanId,
command.PaymentMethodId
);

await _eventStore.SaveEvents(subscription.Id, subscription.GetUncommittedEvents());

return subscription.Id;
}
}

// Query side - read operations
public class SubscriptionProjectionHandler :
IEventHandler<SubscriptionCreated>,
IEventHandler<SubscriptionUpdated>
{
private readonly ISubscriptionReadModel _readModel;

public async Task Handle(SubscriptionCreated @event)
{
var projection = new SubscriptionProjection
{
Id = @event.SubscriptionId,
CustomerId = @event.CustomerId,
Status = @event.Status,
CreatedAt = @event.Timestamp
};

await _readModel.Insert(projection);
}

public async Task Handle(SubscriptionUpdated @event)
{
await _readModel.Update(@event.SubscriptionId, @event.Changes);
}
}

Testing Webhook Events

Event Simulation

class WebhookEventSimulator {
constructor(apiClient) {
this.apiClient = apiClient;
}

async simulateCustomerLifecycle(customerId) {
// Simulate customer creation
await this.triggerEvent('customer.created', {
id: customerId,
name: 'Test Customer',
email: 'test@example.com'
});

// Simulate subscription creation
await this.triggerEvent('subscription.created', {
id: 'sub_test_123',
customer: customerId,
status: 'active'
});

// Simulate payment success
await this.triggerEvent('payment.succeeded', {
id: 'pay_test_123',
customer: customerId,
amount: 999,
status: 'succeeded'
});

// Simulate payment failure
await this.triggerEvent('payment.failed', {
id: 'pay_test_456',
customer: customerId,
amount: 999,
status: 'failed',
failure_code: 'card_declined'
});
}

async triggerEvent(eventType, data) {
const event = {
id: `evt_test_${Date.now()}`,
type: eventType,
created: Math.floor(Date.now() / 1000),
data: { object: data }
};

// Send to webhook endpoint
await fetch('http://localhost:3000/webhooks/azotte', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event)
});
}
}

Integration Testing

import pytest
from unittest.mock import Mock, patch

class TestWebhookEventHandlers:
@pytest.fixture
def event_processor(self):
return WebhookEventProcessor()

@pytest.mark.asyncio
async def test_customer_created_event(self, event_processor):
# Arrange
event = {
'type': 'customer.created',
'data': {
'object': {
'id': 'cus_test_123',
'email': 'test@example.com',
'name': 'Test Customer'
}
}
}

# Mock external services
with patch.object(event_processor.email_service, 'send_welcome_email') as mock_email:
with patch.object(event_processor.user_service, 'create_from_customer') as mock_user:
# Act
result = await event_processor.process_event(event)

# Assert
assert result['processed'] is True
mock_email.assert_called_once_with('test@example.com')
mock_user.assert_called_once()

@pytest.mark.asyncio
async def test_payment_failed_event_triggers_dunning(self, event_processor):
# Arrange
event = {
'type': 'payment.failed',
'data': {
'object': {
'id': 'pay_test_456',
'customer': 'cus_test_123',
'failure_message': 'Card declined'
}
}
}

# Act & Assert
with patch.object(event_processor.email_service, 'send_payment_failed_email') as mock_email:
await event_processor.process_event(event)
mock_email.assert_called_once_with('cus_test_123', 'Card declined')

Event Analytics

Event Metrics Dashboard

interface EventMetrics {
total_events_processed: number;
events_by_type: Record<string, number>;
processing_latency_ms: number;
error_rate_percentage: number;
failed_events: EventFailure[];
}

interface EventFailure {
event_id: string;
event_type: string;
error_message: string;
retry_count: number;
failed_at: Date;
}

class EventAnalytics {
async getEventMetrics(timeRange: TimeRange): Promise<EventMetrics> {
const events = await this.getEventsInRange(timeRange);

return {
total_events_processed: events.length,
events_by_type: this.groupEventsByType(events),
processing_latency_ms: this.calculateAverageLatency(events),
error_rate_percentage: this.calculateErrorRate(events),
failed_events: await this.getFailedEvents(timeRange)
};
}

generateEventReport(metrics: EventMetrics): string {
return `
Event Processing Report
=====================
Total Events: ${metrics.total_events_processed}
Average Latency: ${metrics.processing_latency_ms}ms
Error Rate: ${metrics.error_rate_percentage}%

Top Event Types:
${Object.entries(metrics.events_by_type)
.sort(([,a], [,b]) => b - a)
.slice(0, 5)
.map(([type, count]) => `- ${type}: ${count}`)
.join('\n')}

Failed Events: ${metrics.failed_events.length}
`;
}
}

Best Practices

Event Handling Guidelines

  1. Idempotent processing - Handle duplicate events gracefully
  2. Fast acknowledgment - Respond to webhooks quickly (< 10s)
  3. Async processing - Use queues for time-consuming operations
  4. Error handling - Implement retry logic with exponential backoff
  5. Event ordering - Don't rely on event delivery order

Performance Optimization

  1. Batch processing - Group similar events for efficiency
  2. Selective processing - Filter events by relevance
  3. Caching - Cache frequently accessed data
  4. Database optimization - Use appropriate indexes
  5. Monitoring - Track processing metrics and latency

Next Steps