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