Event System Guide
Complete guide to monitoring and observing client behavior through the event system.
Overview
The Planning Center API clients emit events throughout their lifecycle, allowing you to monitor requests, track errors, observe rate limits, and build custom observability solutions. The event system uses a publish-subscribe pattern where the client automatically emits events that you can listen to.
Two Ways to Listen to Events
1. Programmatic Listeners (Recommended)
Use client.on() to subscribe to events programmatically:
client.on('request:start', (event) => {
console.log(`Starting: ${event.method} ${event.endpoint}`);
});Advantages:
- More flexible - can add/remove listeners dynamically
- Better for complex applications
- Can have multiple listeners per event type
- Type-safe with TypeScript
2. Config-Based Callbacks
Set up event handlers in the client configuration:
const client = new PcoClient({
auth: { /* ... */ },
events: {
onError: (event) => {
console.error('Error:', event.error);
},
onRequestStart: (event) => {
console.log('Request started:', event.endpoint);
}
}
});Advantages:
- Simple setup for basic use cases
- All handlers defined in one place
- Good for simple scripts
Note: Config callbacks are internally converted to client.on() listeners, so both approaches work the same way.
Event API
Listening to Events
// Add an event listener
client.on('request:start', (event) => {
console.log('Request started:', event);
});
// Listen to multiple event types
client.on('request:complete', handleComplete);
client.on('request:error', handleError);Removing Event Listeners
// Remove a specific listener (must be the same function reference)
const handler = (event) => console.log(event);
client.on('request:start', handler);
client.off('request:start', handler);
// Remove all listeners for a specific event type
client.removeAllListeners('request:start');
// Remove all listeners for all event types
client.removeAllListeners();Querying Listeners
// Get the number of listeners for an event type
const count = client.listenerCount('request:start');
console.log(`${count} listeners for request:start`);
// Get all registered event types
const types = client.eventTypes();
console.log('Registered event types:', types);
// ['request:start', 'request:complete', 'error', ...]Event Types
Request Events
request:start
Emitted when an HTTP request begins.
client.on('request:start', (event) => {
// event.type: 'request:start'
// event.timestamp: '2025-01-27T12:00:00.000Z'
// event.requestId: 'req_1234567890_1'
// event.endpoint: '/people/v2/people'
// event.method: 'GET'
// event.params?: { per_page: 25, order: 'created_at' } (when query params were sent)
console.log(`[${event.requestId}] ${event.method} ${event.endpoint}`);
});Properties:
type:'request:start'timestamp: ISO 8601 timestamprequestId: Unique request identifierendpoint: API endpoint pathmethod: HTTP method (GET, POST, PUT, DELETE, PATCH)params(optional): Query params sent (e.g.per_page,order). Omitted when none.
request:complete
Emitted when an HTTP request completes successfully.
client.on('request:complete', (event) => {
// event.type: 'request:complete'
// event.timestamp: '2025-01-27T12:00:00.150Z'
// event.requestId: 'req_1234567890_1'
// event.endpoint: '/people/v2/people'
// event.method: 'GET'
// event.status: 200
// event.duration: 150 (milliseconds)
// event.params?: query params sent (when present)
// event.retryCount?: number of retries before success (when > 0)
// event.rateLimitRemaining?: from response headers (when API sends it)
// event.rateLimitLimit?: from response headers (when API sends it)
// event.responseSummary?: e.g. '25 items' or 'Person:abc123' (when derivable from response)
console.log(`[${event.requestId}] ${event.status} in ${event.duration}ms${event.responseSummary ? ` ${event.responseSummary}` : ''}`);
});Properties:
type:'request:complete'timestamp: ISO 8601 timestamprequestId: Unique request identifierendpoint: API endpoint pathmethod: HTTP methodstatus: HTTP status code (200, 201, etc.)duration: Request duration in millisecondsparams(optional): Query params sent. Omitted when none.retryCount(optional): Number of retries before success (e.g. after 429/401). Present only when > 0.rateLimitRemaining(optional): Rate limit remaining from response headers. Present when the API sends it.rateLimitLimit(optional): Rate limit cap from response headers. Present when the API sends it.responseSummary(optional): Brief summary of the response—e.g."25 items"for a list,"Person:abc123"for a single resource. Omitted for empty or non-JSON responses.
request:error
Emitted when an HTTP request fails.
client.on('request:error', (event) => {
// event.type: 'request:error'
// event.timestamp: '2025-01-27T12:00:00.100Z'
// event.requestId: 'req_1234567890_1'
// event.endpoint: '/people/v2/people'
// event.method: 'GET'
// event.error: Error object
// event.params?: query params sent (when present)
console.error(`[${event.requestId}] Error:`, event.error);
});Properties:
type:'request:error'timestamp: ISO 8601 timestamprequestId: Unique request identifierendpoint: API endpoint pathmethod: HTTP methoderror: Error object (may bePcoApiErrororPcoError)params(optional): Query params sent. Omitted when none.
Authentication Events
auth:success
Emitted when authentication succeeds.
client.on('auth:success', (event) => {
// event.type: 'auth:success'
// event.timestamp: '2025-01-27T12:00:00.000Z'
// event.authType: 'oauth' | 'basic'
console.log(`Authentication successful (${event.authType})`);
});Properties:
type:'auth:success'timestamp: ISO 8601 timestampauthType:'oauth'or'basic'
auth:failure
Emitted when authentication fails.
client.on('auth:failure', (event) => {
// event.type: 'auth:failure'
// event.timestamp: '2025-01-27T12:00:00.000Z'
// event.authType: 'oauth' | 'basic'
// event.error: Error object
console.error(`Authentication failed (${event.authType}):`, event.error);
});Properties:
type:'auth:failure'timestamp: ISO 8601 timestampauthType:'oauth'or'basic'error: Error object
auth:refresh
Emitted when an OAuth token refresh is attempted.
client.on('auth:refresh', (event) => {
// event.type: 'auth:refresh'
// event.timestamp: '2025-01-27T12:00:00.000Z'
// event.authType: 'oauth'
// event.success: boolean
if (event.success) {
console.log('Token refreshed successfully');
} else {
console.error('Token refresh failed');
}
});Properties:
type:'auth:refresh'timestamp: ISO 8601 timestampauthType:'oauth'success:trueif refresh succeeded,falseotherwise
Rate Limit Events
rate:limit
Emitted when rate limit information is updated (typically after each request).
client.on('rate:limit', (event) => {
// event.type: 'rate:limit'
// event.timestamp: '2025-01-27T12:00:00.000Z'
// event.limit: 100
// event.remaining: 95
// event.resetTime: '2025-01-27T12:00:20.000Z'
if (event.remaining < 10) {
console.warn(`Low rate limit: ${event.remaining}/${event.limit} remaining`);
}
});Properties:
type:'rate:limit'timestamp: ISO 8601 timestamplimit: Total rate limit (typically 100)remaining: Remaining requests in current windowresetTime: ISO 8601 timestamp when the rate limit window resets
rate:available
Emitted when rate limit capacity becomes available.
client.on('rate:available', (event) => {
// event.type: 'rate:available'
// event.timestamp: '2025-01-27T12:00:00.000Z'
// event.limit: 100
// event.remaining: 50
console.log(`Rate limit available: ${event.remaining}/${event.limit}`);
});Properties:
type:'rate:available'timestamp: ISO 8601 timestamplimit: Total rate limitremaining: Remaining requests
Cache Events
cache:hit
Emitted when a cache lookup succeeds.
client.on('cache:hit', (event) => {
// event.type: 'cache:hit'
// event.timestamp: '2025-01-27T12:00:00.000Z'
// event.key: 'field:definition:slug:BIRTHDATE'
console.log(`Cache hit: ${event.key}`);
});Properties:
type:'cache:hit'timestamp: ISO 8601 timestampkey: Cache key that was hit
cache:miss
Emitted when a cache lookup fails (value not in cache).
client.on('cache:miss', (event) => {
// event.type: 'cache:miss'
// event.timestamp: '2025-01-27T12:00:00.000Z'
// event.key: 'field:definition:slug:BIRTHDATE'
console.log(`Cache miss: ${event.key}`);
});Properties:
type:'cache:miss'timestamp: ISO 8601 timestampkey: Cache key that was missed
cache:set
Emitted when a value is stored in the cache.
client.on('cache:set', (event) => {
// event.type: 'cache:set'
// event.timestamp: '2025-01-27T12:00:00.000Z'
// event.key: 'field:definition:slug:BIRTHDATE'
// event.ttl?: 300000 (optional, time-to-live in milliseconds)
console.log(`Cache set: ${event.key} (TTL: ${event.ttl}ms)`);
});Properties:
type:'cache:set'timestamp: ISO 8601 timestampkey: Cache key that was setttl: Optional time-to-live in milliseconds
cache:invalidate
Emitted when a cache entry is invalidated (removed).
client.on('cache:invalidate', (event) => {
// event.type: 'cache:invalidate'
// event.timestamp: '2025-01-27T12:00:00.000Z'
// event.key: 'field:definition:slug:BIRTHDATE'
console.log(`Cache invalidated: ${event.key}`);
});Properties:
type:'cache:invalidate'timestamp: ISO 8601 timestampkey: Cache key that was invalidated
Error Events
error
Emitted when any error occurs (general error event, separate from request:error).
client.on('error', (event) => {
// event.type: 'error'
// event.timestamp: '2025-01-27T12:00:00.000Z'
// event.error: Error object
// event.operation: 'people.create'
// event.context?: { /* optional context */ }
console.error(`Error in ${event.operation}:`, event.error);
if (event.context) {
console.error('Context:', event.context);
}
});Properties:
type:'error'timestamp: ISO 8601 timestamperror: Error objectoperation: String describing the operation (e.g.,'people.create')context: Optional context object with additional information
Common Patterns
Request Logging
Log all requests with timing and context. Use the optional event fields for richer logs:
const requestLog: Array<{ id: string; method: string; endpoint: string; duration?: number; status?: number }> = [];
client.on('request:start', (event) => {
requestLog.push({
id: event.requestId,
method: event.method,
endpoint: event.endpoint,
});
});
client.on('request:complete', (event) => {
const logEntry = requestLog.find(log => log.id === event.requestId);
if (logEntry) {
logEntry.duration = event.duration;
logEntry.status = event.status;
const parts = [`✓ ${event.method} ${event.endpoint} - ${event.status} (${event.duration}ms)`];
if (event.responseSummary) parts.push(event.responseSummary);
if (event.params && Object.keys(event.params).length > 0) parts.push(JSON.stringify(event.params));
if (event.retryCount != null && event.retryCount > 0) parts.push(`retries: ${event.retryCount}`);
if (event.rateLimitRemaining != null) parts.push(`rate ${event.rateLimitRemaining}/${event.rateLimitLimit ?? '?'}`);
console.log(parts.join(' '));
}
});
client.on('request:error', (event) => {
const logEntry = requestLog.find(log => log.id === event.requestId);
if (logEntry) {
const paramsStr = event.params && Object.keys(event.params).length > 0 ? ` ${JSON.stringify(event.params)}` : '';
console.error(`✗ ${event.method} ${event.endpoint}${paramsStr} - Error: ${event.error.message}`);
}
});Error Tracking Integration
Send errors to an error tracking service:
import * as Sentry from '@sentry/node';
client.on('error', (event) => {
Sentry.captureException(event.error, {
tags: {
operation: event.operation,
},
extra: event.context,
});
});
client.on('auth:failure', (event) => {
Sentry.captureException(event.error, {
level: 'error',
tags: {
authType: event.authType,
type: 'authentication_failure',
},
});
});Rate Limit Monitoring
Monitor and alert on rate limit status:
client.on('rate:limit', (event) => {
const percentage = (event.remaining / event.limit) * 100;
if (percentage < 10) {
console.warn(`⚠️ Rate limit critical: ${event.remaining}/${event.limit} (${percentage.toFixed(1)}%)`);
// Send alert to monitoring system
} else if (percentage < 25) {
console.warn(`⚠️ Rate limit low: ${event.remaining}/${event.limit} (${percentage.toFixed(1)}%)`);
}
// Calculate time until reset
const resetTime = new Date(event.resetTime);
const now = new Date();
const msUntilReset = resetTime.getTime() - now.getTime();
console.log(`Rate limit resets in ${Math.ceil(msUntilReset / 1000)} seconds`);
});Performance Monitoring
Track request performance:
const performanceMetrics = {
totalRequests: 0,
totalDuration: 0,
slowRequests: [] as Array<{ endpoint: string; duration: number }>,
};
client.on('request:complete', (event) => {
performanceMetrics.totalRequests++;
performanceMetrics.totalDuration += event.duration;
// Track slow requests (> 1 second)
if (event.duration > 1000) {
performanceMetrics.slowRequests.push({
endpoint: event.endpoint,
duration: event.duration,
});
}
});
// Log metrics periodically
setInterval(() => {
const avgDuration = performanceMetrics.totalDuration / performanceMetrics.totalRequests;
console.log(`Performance: ${performanceMetrics.totalRequests} requests, avg ${avgDuration.toFixed(0)}ms`);
if (performanceMetrics.slowRequests.length > 0) {
console.log(`Slow requests: ${performanceMetrics.slowRequests.length}`);
}
}, 60000); // Every minuteConditional Event Handling
Only listen to events in development:
if (process.env.NODE_ENV === 'development') {
client.on('request:start', (event) => {
console.log(`[DEV] ${event.method} ${event.endpoint}`);
});
client.on('request:complete', (event) => {
console.log(`[DEV] ✓ ${event.status} (${event.duration}ms)`);
});
}Cleanup on Application Shutdown
Remove all listeners when shutting down:
process.on('SIGTERM', () => {
console.log('Shutting down...');
client.removeAllListeners();
process.exit(0);
});Best Practices
1. Use TypeScript for Type Safety
The event system is fully typed. Use TypeScript to get autocomplete and type checking:
import type { RequestStartEvent, RequestCompleteEvent } from '@rachelallyson/planning-center-people-ts';
client.on('request:start', (event: RequestStartEvent) => {
// TypeScript knows event has: type, timestamp, requestId, endpoint, method
console.log(event.endpoint);
});2. Store Handler References for Cleanup
If you need to remove listeners later, store the handler function:
const requestHandler = (event: RequestStartEvent) => {
console.log('Request:', event.endpoint);
};
client.on('request:start', requestHandler);
// Later...
client.off('request:start', requestHandler);3. Handle Async Operations in Handlers
Event handlers can be async, but errors in handlers are caught and logged:
client.on('error', async (event) => {
// This is safe - errors are caught internally
await sendToErrorTrackingService(event.error);
});4. Don’t Block the Event Loop
Keep event handlers lightweight. For heavy operations, use queues or background workers:
// ❌ Bad: Blocks event loop
client.on('request:complete', (event) => {
heavySynchronousOperation(event);
});
// ✅ Good: Non-blocking
client.on('request:complete', (event) => {
setImmediate(() => {
heavySynchronousOperation(event);
});
});5. Use Request IDs for Correlation
Use requestId to correlate start and complete events:
const activeRequests = new Map<string, { startTime: number; endpoint: string }>();
client.on('request:start', (event) => {
activeRequests.set(event.requestId, {
startTime: Date.now(),
endpoint: event.endpoint,
});
});
client.on('request:complete', (event) => {
const request = activeRequests.get(event.requestId);
if (request) {
const actualDuration = Date.now() - request.startTime;
console.log(`Request ${event.requestId} took ${actualDuration}ms`);
activeRequests.delete(event.requestId);
}
});Troubleshooting
Events Not Firing
If events aren’t firing, check:
- Listeners are registered: Use
client.eventTypes()to see registered event types - Listener count: Use
client.listenerCount('event:type')to verify listeners - Event type spelling: Event types are case-sensitive and use colons (e.g.,
'request:start')
// Debug event listeners
console.log('Registered event types:', client.eventTypes());
console.log('Listeners for request:start:', client.listenerCount('request:start'));Memory Leaks
If you’re adding many listeners, remember to remove them:
// ❌ Bad: Creates new listener on every call
function makeRequest() {
client.on('request:complete', () => console.log('Done'));
client.people.getAll();
}
// ✅ Good: Reuse listener or remove after use
const handler = () => console.log('Done');
client.on('request:complete', handler);
client.people.getAll();
// Remove when done
client.off('request:complete', handler);Related Documentation
- Configuration Reference - Config-based event handlers
- Error Handling Guide - Error event handling patterns
- Quick Start Guide - Basic event monitoring examples