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
- Idempotent processing - Handle duplicate events gracefully
- Fast acknowledgment - Respond to webhooks quickly (< 10s)
- Async processing - Use queues for time-consuming operations
- Error handling - Implement retry logic with exponential backoff
- Event ordering - Don't rely on event delivery order
Performance Optimization
- Batch processing - Group similar events for efficiency
- Selective processing - Filter events by relevance
- Caching - Cache frequently accessed data
- Database optimization - Use appropriate indexes
- Monitoring - Track processing metrics and latency
Next Steps
- Learn about Webhook Security
- Understand Retry Policy
- Explore Webhooks Overview