Employee Offboarding Workflow

Revoke access when employees leave while maintaining audit trail in 5 minutes

5 mins
Beginner

Complete code to revoke employee access when they leave. Fast, secure, and maintains full audit trail.

The Workflow

Scenario: Employee's last day is Friday. Revoke all building access immediately.

What we'll do

Disable person record (fastest method)
Verify access is revoked
Maintain audit trail

Time: About 1 second per employee

Alternative: Disable credentials individually (slower, more granular)

Why Disable (Not Delete)?

Disabling

Keeps audit trail intact
Can see historical access events
Can re-enable if needed
Maintains compliance records

Deleting

Removes from audit trail
Can't see historical access
Permanent action
Use only for data retention compliance

Recommended: Always disable, never delete.

Quick Offboarding (Disable Person)

Fastest method - disables person and all their credentials:

Bash Script

bash
#!/bin/bash
# Employee Offboarding Script
# Usage: ./offboard_employee.sh 12345

PERSON_ID=$1
ACCESS_TOKEN="your_access_token_here"
BASE_URL="https://api.doorflow.com/api/3"

echo "Offboarding person ID: $PERSON_ID..."

# Disable person (disables all credentials automatically)
curl -s -X PUT "$BASE_URL/people/$PERSON_ID" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": false
  }' | jq '.'

echo "[OK] Person disabled"
echo "[OK] All access revoked"
echo ""
echo "Person record retained for audit trail."
echo "Physical card should be collected during exit interview."

Save as: offboard_employee.sh

Run:

bash
chmod +x offboard_employee.sh
./offboard_employee.sh 12345

JavaScript Version

javascript
async function offboardEmployee(personId) {
  const accessToken = process.env.DOORFLOW_ACCESS_TOKEN;
  const baseURL = 'https://api.doorflow.com/api/3';

  try {
    console.log(`Offboarding person ID: ${personId}...`);

    // Disable person
    const response = await fetch(`${baseURL}/people/${personId}`, {
      method: 'PUT',
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        enabled: false,
      }),
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(`Failed to offboard: ${error.error || error.message}`);
    }

    const person = await response.json();
    console.log(`[OK] Person disabled: ${person.first_name} ${person.last_name}`);
    console.log('[OK] All access revoked');

    return {
      personId: person.id,
      firstName: person.first_name,
      lastName: person.last_name,
      offboardedAt: new Date(),
    };

  } catch (error) {
    console.error('Offboarding failed:', error);
    throw error;
  }
}

// Usage
offboardEmployee(12345)
  .then(result => {
    console.log('Offboarding complete:', result);
    console.log('\nNext steps:');
    console.log('1. Collect physical access card');
    console.log('2. Update HR system');
    console.log('3. Verify access revoked at a door');
  })
  .catch(error => console.error('Error:', error.message));

Alternative: Disable Credentials Individually

More granular approach - disable each credential separately:

javascript
async function offboardEmployeeDetailed(personId) {
  const accessToken = process.env.DOORFLOW_ACCESS_TOKEN;
  const baseURL = 'https://api.doorflow.com/api/3';

  try {
    // Step 1: Get all credentials
    console.log('Retrieving credentials...');

    const credResponse = await fetch(`${baseURL}/people/${personId}/credentials`, {
      headers: {
        'Authorization': `Bearer ${accessToken}`,
      },
    });

    if (!credResponse.ok) {
      throw new Error(`Failed to get credentials: ${await credResponse.text()}`);
    }

    const { credentials } = await credResponse.json();
    console.log(`Found ${credentials.length} credentials`);

    // Step 2: Disable each credential
    for (const credential of credentials) {
      await fetch(`${baseURL}/people/${personId}/credentials/${credential.id}`, {
        method: 'PUT',
        headers: {
          'Authorization': `Bearer ${accessToken}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          person_credential: {
            enabled: false,
          },
        }),
      });

      console.log(`[OK] Disabled ${credential.credential_type_name} (${credential.value})`);
    }

    // Step 3: Remove from all groups (optional)
    await fetch(`${baseURL}/people/${personId}`, {
      method: 'PUT',
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        group_ids: [], // Remove from all groups
      }),
    });

    console.log('[OK] Removed from all groups');
    console.log('[OK] Offboarding complete');

    return {
      personId,
      credentialsDisabled: credentials.length,
      offboardedAt: new Date(),
    };

  } catch (error) {
    console.error('Offboarding failed:', error);
    throw error;
  }
}

// Usage
offboardEmployeeDetailed(12345)
  .then(result => console.log('Detailed offboarding complete:', result));

When to use detailed method

Need to log exactly which credentials were disabled
Want to disable only certain credentials (not all)
Maintaining separate audit trail per credential

Bulk Offboarding

Offboard multiple employees at once:

javascript
async function bulkOffboardEmployees(personIds) {
  console.log(`Offboarding ${personIds.length} employees...`);

  const results = [];

  for (const personId of personIds) {
    try {
      const result = await offboardEmployee(personId);
      results.push({ success: true, ...result });

      // Small delay to avoid rate limits
      await new Promise(resolve => setTimeout(resolve, 1000));

    } catch (error) {
      console.error(`Failed to offboard person ${personId}:`, error.message);
      results.push({
        success: false,
        personId: personId,
        error: error.message,
      });
    }
  }

  // Summary
  const successful = results.filter(r => r.success);
  const failed = results.filter(r => !r.success);

  console.log(`\n[OK] Bulk offboarding complete`);
  console.log(`Successful: ${successful.length}`);
  console.log(`Failed: ${failed.length}`);

  return results;
}

// Usage
const employeesToOffboard = [12345, 12346, 12347];
bulkOffboardEmployees(employeesToOffboard)
  .then(results => {
    // Log results
    console.log('Results:', results);
  });

Scheduled Offboarding

Automatically offboard employee on their last day:

javascript
async function scheduleOffboarding(personId, lastDay) {
  // Store in database for cron job to process
  await db.insert('scheduled_offboarding', {
    person_id: personId,
    offboard_date: lastDay,
    status: 'pending',
  });

  console.log(`Offboarding scheduled for person ${personId} on ${lastDay}`);
}

// Cron job runs daily at 5 PM
async function processScheduledOffboarding() {
  const today = new Date().toISOString().split('T')[0];

  // Get employees scheduled for offboarding today
  const scheduled = await db.query(
    'SELECT person_id FROM scheduled_offboarding WHERE offboard_date = ? AND status = ?',
    [today, 'pending']
  );

  console.log(`Processing ${scheduled.length} scheduled offboardings...`);

  for (const row of scheduled) {
    try {
      await offboardEmployee(row.person_id);

      // Mark as complete
      await db.update('scheduled_offboarding', {
        status: 'completed',
        completed_at: new Date(),
      }, { person_id: row.person_id });

    } catch (error) {
      console.error(`Failed to offboard ${row.person_id}:`, error.message);

      // Mark as failed
      await db.update('scheduled_offboarding', {
        status: 'failed',
        error: error.message,
      }, { person_id: row.person_id });
    }
  }
}

// Usage: Schedule offboarding
const lastDay = new Date('2024-06-30');
scheduleOffboarding(12345, lastDay);

// Usage: Process scheduled (run via cron daily)
// 0 17 * * * node process_offboarding.js
processScheduledOffboarding();

Verify Access Revoked

Always verify offboarding succeeded:

javascript
async function verifyOffboarding(personId) {
  const accessToken = process.env.DOORFLOW_ACCESS_TOKEN;
  const baseURL = 'https://api.doorflow.com/api/3';

  // Check person is disabled
  const personResponse = await fetch(`${baseURL}/people/${personId}`, {
    headers: { 'Authorization': `Bearer ${accessToken}` },
  });

  const person = await personResponse.json();

  if (person.enabled === false) {
    console.log(`[OK] Person is disabled`);
  } else {
    console.log(`[WARNING] Person is still enabled!`);
    return false;
  }

  // Check credentials are disabled
  const credResponse = await fetch(`${baseURL}/people/${personId}/credentials`, {
    headers: { 'Authorization': `Bearer ${accessToken}` },
  });

  const { credentials } = await credResponse.json();
  const enabledCredentials = credentials.filter(c => c.enabled);

  if (enabledCredentials.length === 0) {
    console.log(`[OK] All credentials disabled`);
  } else {
    console.log(`[WARNING] ${enabledCredentials.length} credentials still enabled!`);
    return false;
  }

  return true;
}

// Usage
offboardEmployee(12345)
  .then(() => verifyOffboarding(12345))
  .then(verified => {
    if (verified) {
      console.log('[OK] Offboarding verified');
    } else {
      console.log('[ERROR] Offboarding verification failed');
    }
  });

Integration with HR System

Automatically offboard when HR system updates:

javascript
// When employee marked as terminated in HR system
hrSystem.on('employee.terminated', async (employee) => {
  try {
    // Find person in DoorFlow by email
    const searchResponse = await fetch(
      `${baseURL}/people?email=${employee.workEmail}`,
      {
        headers: { 'Authorization': `Bearer ${accessToken}` },
      }
    );

    const { people } = await searchResponse.json();

    if (people.length === 0) {
      console.log(`No DoorFlow person found for ${employee.workEmail}`);
      return;
    }

    const person = people[0];

    // Offboard in DoorFlow
    await offboardEmployee(person.id);

    // Log in HR system
    await hrSystem.addNote(
      employee.id,
      `Building access revoked in DoorFlow on ${new Date().toISOString()}`
    );

    // Notify facilities to collect card
    await emailService.send({
      to: 'facilities@company.com',
      subject: `Collect access card from ${employee.firstName} ${employee.lastName}`,
      body: `Employee's last day is ${employee.lastDay}. Please collect building access card.`,
    });

    console.log(`[OK] ${employee.firstName} ${employee.lastName} offboarded`);

  } catch (error) {
    // Notify HR of failure
    await hrSystem.addNote(
      employee.id,
      `DoorFlow offboarding failed: ${error.message}. Manual action required.`
    );
  }
});

Best Practices

1. Offboard immediately on last day

Don't wait weeks after employee leaves
Schedule for 5 PM on last working day
Revoke before collecting card (prevents unauthorized use)

2. Collect physical cards

Include in exit interview checklist
Track card collection in your system
Return card to inventory for reuse

3. Maintain audit trail

Never delete person records
Always disable, not delete
Log offboarding action in your system

4. Verify offboarding:

javascript
async function offboardWithVerification(personId) {
  await offboardEmployee(personId);
  const verified = await verifyOffboarding(personId);

  if (!verified) {
    throw new Error('Offboarding verification failed - manual review required');
  }

  return true;
}

5. Handle errors gracefully:

javascript
try {
  await offboardEmployee(personId);
} catch (error) {
  // Log error
  console.error('Offboarding error:', error);

  // Notify security team
  await notifySecurityTeam(`Manual offboarding required for person ${personId}: ${error.message}`);

  // Create manual task
  await createManualTask({
    type: 'offboarding',
    personId: personId,
    priority: 'high',
    reason: error.message,
  });
}

Common Errors

"Person not found"

  • Person ID doesn't exist
  • Double-check person ID
  • Search by email: GET /api/3/people?email=user@example.com

"Insufficient permissions"

  • OAuth scope account.person required
  • Check your access token scopes

"Person already disabled"

  • Person was already offboarded
  • Verify in DoorFlow UI or via API

Offboarding Checklist

Before offboarding

Verify person ID is correct
Confirm this is the employee's last day
Check for scheduled offboarding (avoid duplicates)

During offboarding

Disable person record
Verify offboarding succeeded
Log action in your system

After offboarding

Collect physical access card
Return card to inventory
Update HR system
Verify no access attempts in events log

Required OAuth Scopes

  • account.person - Update person record to disable
  • account.person (optional) - If disabling credentials individually

Next Steps

[Employee Onboarding] - Onboard new employees
[Contractor Access] - Time-limited access revocation
[Events and Audit Trail] - Monitor offboarded employee access attempts

Learn more

[Core Resources] - Understanding people and credentials
[Access Control Model] - How permissions work