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
Related workflows
•
[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