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