Contractor Temporary Access

Grant time-limited access to contractors for weeks or months in 5 minutes

5 mins
Intermediate

Grant contractors access for weeks or months with automatic expiration. Similar to employees but with time limits.

The Workflow

Scenario: Contractor needs access for 3 months to specific areas (loading dock + workshop).

What we'll do

Create contractor record
Assign to "Contractors" group
Issue card or mobile credential
Create extended reservation (90 days)
Schedule automatic revocation when contract ends

Time: About 3-4 seconds per contractor

When to Use

Contractors (vs Visitors vs Employees)

Visitors: 1 day → Use PIN
Contractors: 1 week to 6 months → Use card + reservation
Employees: Permanent → Use card, no reservation

Complete Implementation

javascript
async function grantContractorAccess(
  firstName,
  lastName,
  email,
  cardNumber,
  channelIds,
  endDate
) {
  const accessToken = process.env.DOORFLOW_ACCESS_TOKEN;
  const baseURL = 'https://api.doorflow.com/api/3';

  try {
    console.log(`Granting contractor access: ${firstName} ${lastName}`);
    console.log(`Access until: ${endDate.toLocaleDateString()}`);

    // Step 1: Create contractor
    const personResponse = await fetch(`${baseURL}/people`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        first_name: firstName,
        last_name: lastName,
        email: email,
        enabled: true,
        group_ids: [3], // Contractors group (create if needed)
      }),
    });

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

    const person = await personResponse.json();
    console.log(`[OK] Contractor created (ID: ${person.id})`);

    // Step 2: Issue card credential
    const cardResponse = await fetch(`${baseURL}/people/${person.id}/credentials`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        person_credential: {
          credential_type_id: 5, // Card
          value: cardNumber,
          enabled: true,
        },
      }),
    });

    if (!cardResponse.ok) {
      throw new Error(`Failed to issue card: ${await cardResponse.text()}`);
    }

    const card = await cardResponse.json();
    console.log(`[OK] Card issued (Number: ${cardNumber})`);

    // Step 3: Issue backup PIN
    const pinResponse = await fetch(`${baseURL}/people/${person.id}/credentials`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        person_credential: {
          credential_type_id: 6, // PIN
          value: '******', // Auto-generate
          enabled: true,
        },
      }),
    });

    const pin = await pinResponse.json();
    console.log(`[OK] PIN issued: ${pin.value}`);

    // Step 4: Create reservation until contract end date
    const reservationResponse = await fetch(`${baseURL}/reservations`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        person_id: person.id,
        channel_ids: channelIds,
        start_time: new Date().toISOString(),
        end_time: endDate.toISOString(),
      }),
    });

    if (!reservationResponse.ok) {
      throw new Error(`Failed to create reservation: ${await reservationResponse.text()}`);
    }

    const reservation = await reservationResponse.json();
    console.log(`[OK] Reservation created (expires: ${endDate.toLocaleDateString()})`);

    // Step 5: Schedule automatic offboarding
    await scheduleContractorOffboarding(person.id, endDate);

    console.log('[OK] Contractor access granted!');

    return {
      personId: person.id,
      firstName: firstName,
      lastName: lastName,
      email: email,
      cardNumber: cardNumber,
      pin: pin.value,
      contractEnds: endDate,
      reservationId: reservation.id,
    };

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

async function scheduleContractorOffboarding(personId, endDate) {
  // Store in database for cron job to process
  await db.insert('scheduled_offboarding', {
    person_id: personId,
    offboard_date: endDate,
    status: 'pending',
    type: 'contractor',
    created_at: new Date(),
  });

  console.log(`[OK] Offboarding scheduled for ${endDate.toLocaleDateString()}`);
}

// Usage: 3-month contractor starting today
const contractEndDate = new Date();
contractEndDate.setMonth(contractEndDate.getMonth() + 3);

grantContractorAccess(
  'Bob',
  'Builder',
  'bob@contractor.com',
  '9876543210',
  [1340, 1342], // Loading dock + workshop only
  contractEndDate
).then(result => {
  console.log('\nContractor access granted:', result);
  console.log(`\nNext steps:`);
  console.log(`1. Give contractor card: ${result.cardNumber}`);
  console.log(`2. Send PIN backup: ${result.pin}`);
  console.log(`3. Access expires: ${result.contractEnds.toLocaleDateString()}`);
});

Bash Script Version

Simple command-line script:

bash
#!/bin/bash
# Grant Contractor Access
# Usage: ./grant_contractor.sh "Bob" "Builder" "bob@contractor.com" "9876543210" "1340,1342" "90"

FIRST_NAME=$1
LAST_NAME=$2
EMAIL=$3
CARD_NUMBER=$4
CHANNEL_IDS=$5  # Comma-separated
DAYS=$6         # How many days from now

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

# Calculate end date
if [[ "$OSTYPE" == "darwin"* ]]; then
  # macOS
  END_TIME=$(date -u -v+${DAYS}d '+%Y-%m-%dT%H:%M:%SZ')
else
  # Linux
  END_TIME=$(date -u -d "+${DAYS} days" '+%Y-%m-%dT%H:%M:%SZ')
fi

START_TIME=$(date -u '+%Y-%m-%dT%H:%M:%SZ')

echo "Granting contractor access: $FIRST_NAME $LAST_NAME"
echo "Contract ends: $END_TIME"

# Create contractor
PERSON_RESPONSE=$(curl -s -X POST "$BASE_URL/people" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"first_name\": \"$FIRST_NAME\",
    \"last_name\": \"$LAST_NAME\",
    \"email\": \"$EMAIL\",
    \"enabled\": true,
    \"group_ids\": [3]
  }")

PERSON_ID=$(echo $PERSON_RESPONSE | jq -r '.id')
echo "[OK] Contractor created (ID: $PERSON_ID)"

# Issue card
curl -s -X POST "$BASE_URL/people/$PERSON_ID/credentials" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"person_credential\": {
      \"credential_type_id\": 5,
      \"value\": \"$CARD_NUMBER\",
      \"enabled\": true
    }
  }" > /dev/null

echo "[OK] Card issued: $CARD_NUMBER"

# Issue PIN
PIN_RESPONSE=$(curl -s -X POST "$BASE_URL/people/$PERSON_ID/credentials" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "person_credential": {
      "credential_type_id": 6,
      "value": "******",
      "enabled": true
    }
  }')

PIN_VALUE=$(echo $PIN_RESPONSE | jq -r '.value')
echo "[OK] PIN issued: $PIN_VALUE"

# Create reservation
curl -s -X POST "$BASE_URL/reservations" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"person_id\": $PERSON_ID,
    \"channel_ids\": [$CHANNEL_IDS],
    \"start_time\": \"$START_TIME\",
    \"end_time\": \"$END_TIME\"
  }" > /dev/null

echo "[OK] Reservation created"
echo ""
echo "Summary:"
echo "  Contractor: $FIRST_NAME $LAST_NAME"
echo "  Card: $CARD_NUMBER"
echo "  PIN: $PIN_VALUE"
echo "  Access expires: $END_TIME"

Automatic Offboarding

Cron job to automatically offboard expired contractors:

javascript
// Run daily at 5 PM to offboard contractors whose contracts ended
async function processExpiredContractors() {
  const today = new Date();
  today.setHours(0, 0, 0, 0);

  try {
    // Get contractors scheduled for offboarding today or earlier
    const expired = await db.query(
      'SELECT person_id, offboard_date FROM scheduled_offboarding WHERE offboard_date <= ? AND status = ? AND type = ?',
      [today, 'pending', 'contractor']
    );

    console.log(`Processing ${expired.length} expired contractors...`);

    for (const row of expired) {
      try {
        // Offboard contractor
        await offboardEmployee(row.person_id);

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

        console.log(`[OK] Contractor ${row.person_id} offboarded`);

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

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

        // Notify admin
        await notifyAdmin(`Failed to offboard contractor ${row.person_id}: ${error.message}`);
      }
    }

    console.log('[OK] Expired contractor processing complete');

  } catch (error) {
    console.error('Expired contractor processing failed:', error);
  }
}

// Cron schedule: 0 17 * * * (every day at 5 PM)
// Add to your cron: node process_expired_contractors.js
processExpiredContractors();

Contract Extension

Extend contractor's access if contract is extended:

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

  try {
    // Get existing reservations
    const resResponse = await fetch(`${baseURL}/reservations?person_id=${personId}`, {
      headers: { 'Authorization': `Bearer ${accessToken}` },
    });

    const { reservations } = await resResponse.json();

    // Update each reservation end time
    for (const reservation of reservations) {
      await fetch(`${baseURL}/reservations/${reservation.id}`, {
        method: 'PUT',
        headers: {
          'Authorization': `Bearer ${accessToken}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          end_time: newEndDate.toISOString(),
        }),
      });

      console.log(`[OK] Reservation ${reservation.id} extended to ${newEndDate.toLocaleDateString()}`);
    }

    // Update scheduled offboarding
    await db.update('scheduled_offboarding', {
      offboard_date: newEndDate,
      status: 'pending',
    }, { person_id: personId });

    console.log('[OK] Contract extended!');

    return {
      personId,
      newEndDate,
      reservationsUpdated: reservations.length,
    };

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

// Usage: Extend for another 2 months
const newEndDate = new Date();
newEndDate.setMonth(newEndDate.getMonth() + 2);

extendContractorAccess(12345, newEndDate)
  .then(result => console.log('Contract extended:', result));

Mobile Credentials for Contractors

For contractors without physical cards:

javascript
async function grantContractorMobileAccess(
  firstName,
  lastName,
  email,
  channelIds,
  endDate
) {
  const accessToken = process.env.DOORFLOW_ACCESS_TOKEN;
  const baseURL = 'https://api.doorflow.com/api/3';

  try {
    // Create contractor
    const personResponse = await fetch(`${baseURL}/people`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        first_name: firstName,
        last_name: lastName,
        email: email,
        enabled: true,
        group_ids: [3],
      }),
    });

    const person = await personResponse.json();

    // Issue mobile credential (PassFlow)
    const mobileResponse = await fetch(`${baseURL}/people/${person.id}/credentials`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        person_credential: {
          credential_type_id: 9, // PassFlow
          value: 'passflow-credential-id', // From PassFlow system
          enabled: true,
        },
      }),
    });

    const mobile = await mobileResponse.json();
    console.log(`[OK] Mobile credential issued`);

    // Issue backup PIN
    const pinResponse = await fetch(`${baseURL}/people/${person.id}/credentials`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        person_credential: {
          credential_type_id: 6,
          value: '******',
          enabled: true,
        },
      }),
    });

    const pin = await pinResponse.json();
    console.log(`[OK] Backup PIN issued: ${pin.value}`);

    // Create reservation
    await fetch(`${baseURL}/reservations`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        person_id: person.id,
        channel_ids: channelIds,
        start_time: new Date().toISOString(),
        end_time: endDate.toISOString(),
      }),
    });

    return {
      personId: person.id,
      email: email,
      pin: pin.value,
      contractEnds: endDate,
    };

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

Best Practices

1. Always set end date

Never give contractors permanent access
Use reservations to enforce automatic expiration
Schedule offboarding reminder before contract ends

2. Limit access to necessary doors:

javascript
// Good: Only areas contractor needs
channelIds: [1340, 1342]  // Loading dock + workshop

// Bad: All doors
// channelIds: allChannels

3. Track contract dates:

javascript
// Store contract info for reporting
await db.insert('contractor_contracts', {
  person_id: personId,
  company_name: 'ABC Contracting',
  start_date: new Date(),
  end_date: endDate,
  contact_email: email,
  approved_by: currentUser.id,
});

4. Issue both card and PIN

Card for daily use (faster)
PIN for backup (when card is forgotten)

5. Collect card on contract end

Include in offboarding workflow
Track card collection
Return card to inventory

Required OAuth Scopes

  • account.person - Create contractor
  • account.person - Issue credentials
  • account.reservation - Time-limited access

Next Steps

[Employee Onboarding] - Permanent employee access
[Visitor Management] - Short-term visitor access
[Employee Offboarding] - Revoke access when contract ends

Learn more

[Card Credentials] - Physical access cards
[Mobile Credentials] - Smartphone-based access
[Credential Comparison] - Choose the right type