Skip to Content
GuidesEvent System Guide

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

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 timestamp
  • requestId: Unique request identifier
  • endpoint: API endpoint path
  • method: 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 timestamp
  • requestId: Unique request identifier
  • endpoint: API endpoint path
  • method: HTTP method
  • status: HTTP status code (200, 201, etc.)
  • duration: Request duration in milliseconds
  • params (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 timestamp
  • requestId: Unique request identifier
  • endpoint: API endpoint path
  • method: HTTP method
  • error: Error object (may be PcoApiError or PcoError)
  • 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 timestamp
  • authType: '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 timestamp
  • authType: '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 timestamp
  • authType: 'oauth'
  • success: true if refresh succeeded, false otherwise

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 timestamp
  • limit: Total rate limit (typically 100)
  • remaining: Remaining requests in current window
  • resetTime: 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 timestamp
  • limit: Total rate limit
  • remaining: 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 timestamp
  • key: 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 timestamp
  • key: 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 timestamp
  • key: Cache key that was set
  • ttl: 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 timestamp
  • key: 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 timestamp
  • error: Error object
  • operation: 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 minute

Conditional 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:

  1. Listeners are registered: Use client.eventTypes() to see registered event types
  2. Listener count: Use client.listenerCount('event:type') to verify listeners
  3. 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);
Last updated on