Events and Audit Trail

Monitor access activity with complete event code reference and filtering strategies

15 mins
Intermediate

Related API Endpoints

Events provide a complete audit trail of all access activity in your DoorFlow account. This guide explains event types, event codes, and how to build powerful monitoring and reporting tools.

What Are Events?

Events are immutable records of activity:

  • Access attempts (successful and failed)
  • Door status changes (online, offline, mode changes)
  • System events (syncs, errors)
  • Credential usage tracking

Events are read-only - they're automatically created by the system and cannot be modified or deleted.

Event Structure

json
{
  "id": 123456,
  "event_code": 12,
  "person_id": 5678,
  "person_name": "Jane Doe",
  "channel_id": 1340,
  "channel_name": "Front Door",
  "credential_id": "cred_abc123",
  "occurred_at": "2024-01-15T14:30:45Z",
  "description": "Jane Doe accessed Front Door",
  "additional_data": {
    "credential_type": "Card",
    "card_number": "1234567890"
  }
}

Key fields

id: Unique event identifier
event_code: Numeric code indicating event type (see reference below)
person_id: Who was involved (null for non-person events)
channel_id: Which door (null for non-channel events)
credential_id: Which credential was used
occurred_at: Timestamp (UTC)
description: Human-readable description
additional_data: Extra context (varies by event type)

Event Codes Reference

Access Events (10-29)

Admit Events (10-18) - Successful Access

Code Name Description
10 Access Granted Person successfully accessed door with credential
12 Access Granted (Schedule) Access granted during scheduled time
13 Access Granted (Reservation) Access granted via time-limited reservation
14 Access Granted (Remote) Door unlocked remotely via API
15 Access Granted (PIN) Access with PIN credential
16 Access Granted (Mobile) Access with mobile credential

Common use cases

Attendance tracking (who entered building)
Security monitoring (successful access)
Billing (contractor hours based on door access)

Example:

json
{
  "event_code": 10,
  "person_id": 12345,
  "person_name": "John Smith",
  "channel_id": 1340,
  "channel_name": "Front Entrance",
  "occurred_at": "2024-01-15T09:15:30Z",
  "description": "John Smith accessed Front Entrance"
}

Reject Events (20-29) - Denied Access

Code Name Description
20 Access Denied Credential rejected (generic)
21 Access Denied (Disabled Person) Person account is disabled
22 Access Denied (Disabled Credential) Credential is disabled
23 Access Denied (No Permission) Person lacks permission for this door
24 Access Denied (Outside Schedule) Access attempted outside permitted hours
25 Access Denied (Invalid Credential) Credential not recognized/invalid format
26 Access Denied (Expired Reservation) Reservation has expired

Common use cases

Security alerts (unauthorized access attempts)
User support (why can't employee access door?)
Audit compliance (tracking denied access)

Example:

json
{
  "event_code": 23,
  "person_id": 12345,
  "person_name": "John Smith",
  "channel_id": 1350,
  "channel_name": "Server Room",
  "occurred_at": "2024-01-15T14:22:10Z",
  "description": "John Smith denied access to Server Room - No Permission"
}

Troubleshooting denied access

Code 21: Enable person account
Code 22: Enable credential
Code 23: Add person to group with access to this door
Code 24: Adjust role schedule or create reservation
Code 25: Check credential value format
Code 26: Extend or recreate reservation

Channel Status Events (40-49)

Code Name Description
40 Channel Online Channel came online
41 Channel Offline Channel went offline
42 Channel Mode: Normal Door set to normal operation
43 Channel Mode: Unlock Door set to permanently unlocked
44 Channel Mode: Lockdown Door set to lockdown (no access)
45 Channel Tamper Alert Physical tampering detected
46 Door Forced Open Door opened without valid credential
47 Door Held Open Door left open too long

Common use cases

Infrastructure monitoring (channel health)
Security alerts (tamper, forced entry)
Maintenance (offline channels)

Example:

json
{
  "event_code": 41,
  "channel_id": 1340,
  "channel_name": "Front Door",
  "occurred_at": "2024-01-15T03:15:00Z",
  "description": "Front Door went offline"
}

System Events (60+)

Code Name Description
60 Sync Started Data synchronization began
61 Sync Completed Data synchronization finished
62 Sync Failed Data synchronization error
70 Configuration Change System configuration updated

Common use cases

System health monitoring
Debugging sync issues
Change tracking

Querying Events

Basic Query

Request:

bash
curl -X GET "https://api.doorflow.com/api/3/events" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response:

json
{
  "events": [
    {
      "id": 123456,
      "event_code": 10,
      "person_id": 12345,
      "person_name": "Jane Doe",
      "channel_id": 1340,
      "channel_name": "Front Door",
      "occurred_at": "2024-01-15T14:30:45Z",
      "description": "Jane Doe accessed Front Door"
    },
    // ... more events
  ],
  "pagination": {
    "page": 1,
    "per_page": 50,
    "total_pages": 10,
    "total_count": 487
  }
}

Filtering by Person

Get all events for a specific person:

bash
# By person ID
curl -X GET "https://api.doorflow.com/api/3/events?person_id=12345" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

# By last name
curl -X GET "https://api.doorflow.com/api/3/events?last_name=Doe" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

# By email
curl -X GET "https://api.doorflow.com/api/3/events?email=jane@example.com" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Use cases

"Show me when this employee accessed the building"
"When did this contractor last visit?"
Individual attendance reports

Filtering by Channel

Get all events for a specific door:

bash
# By channel ID
curl -X GET "https://api.doorflow.com/api/3/events?channel_id=1340" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

# Multiple channels
curl -X GET "https://api.doorflow.com/api/3/events?channel_id=1340&channel_id=1341" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Use cases

"Who accessed the server room today?"
"Show all failed access attempts at this door"
Door-specific security monitoring

Filtering by Time Range

Get events within a specific time window:

bash
# Since a specific time (UTC)
curl -X GET "https://api.doorflow.com/api/3/events?since_utc=2024-01-15T00:00:00Z" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

# Between two times
curl -X GET "https://api.doorflow.com/api/3/events?since_utc=2024-01-15T09:00:00Z&until_utc=2024-01-15T17:00:00Z" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

# Last 24 hours
curl -X GET "https://api.doorflow.com/api/3/events?since_utc=$(date -u -v-24H '+%Y-%m-%dT%H:%M:%SZ')" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Use cases

Daily access reports
After-hours access monitoring
Incident investigation (what happened between 2-3 PM?)

Filtering by Event Code

Get specific types of events:

bash
# Only denied access events (codes 20-29)
curl -X GET "https://api.doorflow.com/api/3/events?event_code=20&event_code=21&event_code=22&event_code=23" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

# Only successful access (codes 10-18)
curl -X GET "https://api.doorflow.com/api/3/events?event_code=10&event_code=12&event_code=13" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

# Channel offline events
curl -X GET "https://api.doorflow.com/api/3/events?event_code=41" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Use cases

Security alerts (denied access only)
Infrastructure monitoring (channel status only)
Attendance tracking (successful access only)

Combining Filters

Build complex queries by combining filters:

bash
# Denied access at specific door in last 24 hours
curl -X GET "https://api.doorflow.com/api/3/events?channel_id=1340&event_code=20&event_code=21&event_code=22&event_code=23&since_utc=2024-01-14T14:00:00Z" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

# Specific person at specific door
curl -X GET "https://api.doorflow.com/api/3/events?person_id=12345&channel_id=1340" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Pagination

Handle large result sets:

bash
# Default: 50 per page
curl -X GET "https://api.doorflow.com/api/3/events" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

# Custom page size
curl -X GET "https://api.doorflow.com/api/3/events?per_page=100&page=2" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

# Skip pagination (get all - use carefully!)
curl -X GET "https://api.doorflow.com/api/3/events?skip_pagination=true" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Best practice: Use pagination for large queries to avoid timeouts.

Common Use Cases

Use Case 1: Daily Access Report

Goal: Generate report of who accessed building today

javascript
async function generateDailyAccessReport() {
  const today = new Date();
  today.setHours(0, 0, 0, 0);

  const since = today.toISOString();

  const response = await fetch(
    `/api/3/events?since_utc=${since}&event_code=10&event_code=12&event_code=13`,
    {
      headers: { 'Authorization': `Bearer ${accessToken}` }
    }
  );

  const { events } = await response.json();

  // Group by person
  const accessByPerson = events.reduce((acc, event) => {
    const key = event.person_id;
    if (!acc[key]) {
      acc[key] = {
        person: event.person_name,
        firstAccess: event.occurred_at,
        lastAccess: event.occurred_at,
        accessCount: 0,
      };
    }

    acc[key].accessCount++;
    acc[key].lastAccess = event.occurred_at;

    return acc;
  }, {});

  console.log('Daily Access Report:');
  Object.values(accessByPerson).forEach(person => {
    console.log(`${person.person}: ${person.accessCount} accesses (First: ${person.firstAccess}, Last: ${person.lastAccess})`);
  });

  return accessByPerson;
}

Use Case 2: Security Alert for Denied Access

Goal: Alert security team when access is denied

javascript
async function monitorDeniedAccess() {
  // Check for denied access in last 5 minutes
  const since = new Date(Date.now() - 5 * 60 * 1000).toISOString();

  const response = await fetch(
    `/api/3/events?since_utc=${since}&event_code=20&event_code=21&event_code=22&event_code=23&event_code=24&event_code=25`,
    {
      headers: { 'Authorization': `Bearer ${accessToken}` }
    }
  );

  const { events } = await response.json();

  for (const event of events) {
    // Alert on suspicious patterns
    if (event.event_code === 25) {
      // Invalid credential - potential security threat
      await sendAlert({
        priority: 'HIGH',
        type: 'Invalid Credential',
        person: event.person_name || 'Unknown',
        channel: event.channel_name,
        time: event.occurred_at,
      });
    } else if (event.event_code === 23) {
      // No permission - might need support
      await sendAlert({
        priority: 'LOW',
        type: 'Permission Issue',
        person: event.person_name,
        channel: event.channel_name,
        message: `${event.person_name} tried to access ${event.channel_name} but lacks permission`,
      });
    }
  }
}

// Run every 5 minutes
setInterval(monitorDeniedAccess, 5 * 60 * 1000);

Use Case 3: Attendance Tracking

Goal: Track employee attendance based on door access

javascript
async function getEmployeeAttendance(personId, date) {
  const startOfDay = new Date(date);
  startOfDay.setHours(0, 0, 0, 0);

  const endOfDay = new Date(date);
  endOfDay.setHours(23, 59, 59, 999);

  const response = await fetch(
    `/api/3/events?person_id=${personId}&since_utc=${startOfDay.toISOString()}&until_utc=${endOfDay.toISOString()}&event_code=10&event_code=12`,
    {
      headers: { 'Authorization': `Bearer ${accessToken}` }
    }
  );

  const { events } = await response.json();

  if (events.length === 0) {
    return { present: false };
  }

  // First and last access
  const firstAccess = events[events.length - 1]; // Events are reverse chronological
  const lastAccess = events[0];

  return {
    present: true,
    checkIn: firstAccess.occurred_at,
    checkOut: lastAccess.occurred_at,
    totalAccessEvents: events.length,
  };
}

// Usage
const attendance = await getEmployeeAttendance(12345, '2024-01-15');
console.log(`Present: ${attendance.present}`);
if (attendance.present) {
  console.log(`Check-in: ${attendance.checkIn}`);
  console.log(`Check-out: ${attendance.checkOut}`);
}

Use Case 4: Infrastructure Monitoring

Goal: Monitor channel health (online/offline status)

javascript
async function monitorChannelHealth() {
  // Get channel status events from last hour
  const since = new Date(Date.now() - 60 * 60 * 1000).toISOString();

  const response = await fetch(
    `/api/3/events?since_utc=${since}&event_code=40&event_code=41`,
    {
      headers: { 'Authorization': `Bearer ${accessToken}` }
    }
  );

  const { events } = await response.json();

  // Identify currently offline channels
  const channelStatus = {};

  events.forEach(event => {
    channelStatus[event.channel_id] = {
      channelName: event.channel_name,
      status: event.event_code === 40 ? 'online' : 'offline',
      lastChange: event.occurred_at,
    };
  });

  // Alert on offline channels
  Object.values(channelStatus).forEach(channel => {
    if (channel.status === 'offline') {
      console.error(`Channel offline: ${channel.channelName} (since ${channel.lastChange})`);
      // sendMaintenanceAlert(channel);
    }
  });

  return channelStatus;
}

Use Case 5: Incremental Sync

Goal: Efficiently sync only new events to external database

javascript
let lastSyncTime = null;

async function syncEventsToDatabase() {
  // Get sync timestamp from database or use default
  lastSyncTime = lastSyncTime || await db.query('SELECT last_sync_time FROM sync_status');

  const since = lastSyncTime || new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();

  const response = await fetch(
    `/api/3/events?since_utc=${since}&skip_pagination=true`,
    {
      headers: { 'Authorization': `Bearer ${accessToken}` }
    }
  );

  const { events } = await response.json();

  console.log(`Syncing ${events.length} new events since ${since}`);

  // Insert into local database
  for (const event of events) {
    await db.insert('events', event);
  }

  // Update sync timestamp
  const newSyncTime = new Date().toISOString();
  await db.update('sync_status', { last_sync_time: newSyncTime });
  lastSyncTime = newSyncTime;

  console.log(`[OK] Sync complete. Next sync from: ${lastSyncTime}`);
}

// Run every 5 minutes
setInterval(syncEventsToDatabase, 5 * 60 * 1000);

Event Analysis Patterns

Count Events by Type

javascript
async function analyzeEventTypes(since) {
  const response = await fetch(
    `/api/3/events?since_utc=${since}&skip_pagination=true`,
    {
      headers: { 'Authorization': `Bearer ${accessToken}` }
    }
  );

  const { events } = await response.json();

  const eventCounts = events.reduce((acc, event) => {
    const code = event.event_code;
    const category =
      code >= 10 && code < 20 ? 'Successful Access' :
      code >= 20 && code < 30 ? 'Denied Access' :
      code >= 40 && code < 50 ? 'Channel Status' :
      'Other';

    acc[category] = (acc[category] || 0) + 1;
    return acc;
  }, {});

  console.log('Event Summary:', eventCounts);
  return eventCounts;
}

Detect Unusual Patterns

javascript
async function detectUnusualActivity() {
  const since = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();

  const response = await fetch(
    `/api/3/events?since_utc=${since}&skip_pagination=true`,
    {
      headers: { 'Authorization': `Bearer ${accessToken}` }
    }
  );

  const { events } = await response.json();

  // Detect multiple failed attempts by same person
  const failedAttempts = {};

  events.filter(e => e.event_code >= 20 && e.event_code < 30).forEach(event => {
    const key = event.person_id || 'unknown';
    failedAttempts[key] = (failedAttempts[key] || 0) + 1;
  });

  // Alert if more than 5 failed attempts
  Object.entries(failedAttempts).forEach(([personId, count]) => {
    if (count > 5) {
      console.warn(`Unusual activity: Person ${personId} has ${count} failed access attempts`);
    }
  });

  // Detect after-hours access
  const afterHoursEvents = events.filter(event => {
    const hour = new Date(event.occurred_at).getHours();
    return hour < 6 || hour > 20; // Before 6 AM or after 8 PM
  });

  console.log(`After-hours access events: ${afterHoursEvents.length}`);

  return {
    failedAttempts,
    afterHoursCount: afterHoursEvents.length,
  };
}

Best Practices

1. Use Incremental Sync

Don't query all events every time - use since_utc:

javascript
// Good: Only get new events
const lastSync = '2024-01-15T14:00:00Z';
const events = await fetchEvents({ since_utc: lastSync });

// Bad: Gets all events every time
const events = await fetchEvents();

2. Filter Early

Apply filters server-side, not client-side:

javascript
// Good: Filter in API request
const deniedEvents = await fetchEvents({ event_code: [20, 21, 22, 23] });

// Bad: Get all events then filter
const allEvents = await fetchEvents();
const deniedEvents = allEvents.filter(e => e.event_code >= 20 && e.event_code < 30);

3. Use Pagination for Large Queries

javascript
// Good: Paginate large queries
async function getAllEvents() {
  let allEvents = [];
  let page = 1;
  let hasMore = true;

  while (hasMore) {
    const { events, pagination } = await fetchEvents({ page, per_page: 100 });
    allEvents = allEvents.concat(events);
    hasMore = page < pagination.total_pages;
    page++;
  }

  return allEvents;
}

// Bad: skip_pagination on unlimited time range
const events = await fetchEvents({ skip_pagination: true }); // Could timeout!

4. Cache Event Counts

javascript
// Cache event counts to avoid repeated queries
const cache = new Map();

async function getEventCount(filters) {
  const cacheKey = JSON.stringify(filters);

  if (cache.has(cacheKey)) {
    const { count, timestamp } = cache.get(cacheKey);

    // Cache for 5 minutes
    if (Date.now() - timestamp < 5 * 60 * 1000) {
      return count;
    }
  }

  const { pagination } = await fetchEvents({ ...filters, per_page: 1 });
  const count = pagination.total_count;

  cache.set(cacheKey, { count, timestamp: Date.now() });

  return count;
}

Quick Reference

Get events:

bash
GET /api/3/events

Query parameters

person_id - Filter by person
channel_id - Filter by channel
event_code - Filter by event type
since_utc - Events after this time
until_utc - Events before this time
page - Page number (default: 1)
per_page - Results per page (default: 50, max: 500)
skip_pagination - Get all results (use carefully!)

Required OAuth scope: account.event.access.readonly

Event code ranges

10-18: Successful access
20-29: Denied access
40-49: Channel status
60+: System events

Next Steps

  • [Common Workflows] - See events used in real scenarios
  • API Reference: Events - Complete endpoint documentation
  • [Error Handling] - Handle API errors when querying events
  • [Webhooks Complete Guide] - Get real-time event notifications

Need Help?

Common questions:

  • Q: How long are events stored? A: Events are stored indefinitely.
  • Q: Can I delete events? A: No, events are immutable for audit compliance.
  • Q: What's the maximum time range? A: No hard limit, but use pagination for large ranges.
  • Q: Can I get real-time events? A: Use webhooks for real-time notifications (see Webhooks guide).