Remote unlock doors for specific people outside normal hours. Perfect for forgotten items, emergency access, or one-time entry.
The Workflow
Scenario: Employee calls at 10 PM - forgot laptop in office, needs to retrieve it.
What we'll do
admit_person to unlock for specific person
Time: About 1-2 seconds
When to Use
After-hours access for
How it works
admit_person unlocks door once for specific person
Quick Implementation
JavaScript Version
async function grantAfterHoursAccess(personId, channelId, reason) {
const accessToken = process.env.DOORFLOW_ACCESS_TOKEN;
const baseURL = 'https://api.doorflow.com/api/3';
try {
console.log(`Granting after-hours access...`);
console.log(`Person: ${personId}`);
console.log(`Channel: ${channelId}`);
console.log(`Reason: ${reason}\n`);
// Admit specific person to specific channel
const response = await fetch(`${baseURL}/channels/${channelId}/admit_person`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
person_id: personId,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Failed to admit person: ${error.error || error.message}`);
}
console.log(`[OK] Access granted to person ${personId} at channel ${channelId}`);
console.log('[OK] Person has 30 seconds to use their credential');
// Log the action for audit
await logAfterHoursAccess(personId, channelId, reason);
return {
personId,
channelId,
grantedAt: new Date(),
reason,
};
} catch (error) {
console.error('After-hours access failed:', error);
throw error;
}
}
async function logAfterHoursAccess(personId, channelId, reason) {
// Log to your audit system
await db.insert('after_hours_log', {
person_id: personId,
channel_id: channelId,
reason: reason,
granted_by: currentUser.id,
granted_at: new Date(),
});
console.log('[OK] Access logged for audit trail');
}
// Usage
grantAfterHoursAccess(12345, 1340, 'Forgot laptop, retrieving from office')
.then(result => {
console.log('\nAccess granted:', result);
console.log('Person should present credential at door now.');
})
.catch(error => console.error('Error:', error.message));
Bash Script
#!/bin/bash
# After-Hours Access Script
# Usage: ./grant_access.sh 12345 1340 "Forgot laptop"
PERSON_ID=$1
CHANNEL_ID=$2
REASON=$3
ACCESS_TOKEN="your_access_token_here"
BASE_URL="https://api.doorflow.com/api/3"
echo "Granting after-hours access..."
echo "Person ID: $PERSON_ID"
echo "Channel ID: $CHANNEL_ID"
echo "Reason: $REASON"
echo ""
# Admit person to channel
curl -s -X POST "$BASE_URL/channels/$CHANNEL_ID/admit_person" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"person_id\": $PERSON_ID
}" | jq '.'
echo ""
echo "[OK] Access granted"
echo "[OK] Person has 30 seconds to use their credential at the door"
Save as: grant_access.sh
Run:
chmod +x grant_access.sh
./grant_access.sh 12345 1340 "Forgot laptop in office"
Find Person and Channel
Helper functions to find IDs when you only have name/email:
async function findPersonByEmail(email) {
const accessToken = process.env.DOORFLOW_ACCESS_TOKEN;
const baseURL = 'https://api.doorflow.com/api/3';
const response = await fetch(`${baseURL}/people?email=${email}`, {
headers: { 'Authorization': `Bearer ${accessToken}` },
});
const { people } = await response.json();
if (people.length === 0) {
throw new Error(`Person not found with email: ${email}`);
}
return people[0];
}
async function findChannelByName(name) {
const accessToken = process.env.DOORFLOW_ACCESS_TOKEN;
const baseURL = 'https://api.doorflow.com/api/3';
const response = await fetch(`${baseURL}/channels`, {
headers: { 'Authorization': `Bearer ${accessToken}` },
});
const { channels } = await response.json();
const channel = channels.find(c =>
c.name.toLowerCase().includes(name.toLowerCase())
);
if (!channel) {
throw new Error(`Channel not found matching: ${name}`);
}
return channel;
}
// Usage: Grant access by name instead of ID
async function grantAccessByName(email, doorName, reason) {
try {
const person = await findPersonByEmail(email);
const channel = await findChannelByName(doorName);
console.log(`Found person: ${person.first_name} ${person.last_name} (ID: ${person.id})`);
console.log(`Found channel: ${channel.name} (ID: ${channel.id})\n`);
return await grantAfterHoursAccess(person.id, channel.id, reason);
} catch (error) {
console.error('Error:', error.message);
throw error;
}
}
// Usage
grantAccessByName('john@company.com', 'Front Door', 'Forgot laptop')
.then(result => console.log('Access granted:', result));
Unlock Door (No Credential Needed)
Alternative: Unlock door completely (anyone can enter):
async function unlockDoor(channelId, durationSeconds = 10) {
const accessToken = process.env.DOORFLOW_ACCESS_TOKEN;
const baseURL = 'https://api.doorflow.com/api/3';
try {
console.log(`Unlocking channel ${channelId} for ${durationSeconds} seconds...`);
// Set channel to unlock mode
await fetch(`${baseURL}/channels/${channelId}/unlock`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
console.log('[OK] Door unlocked - anyone can enter');
// Wait for duration
await new Promise(resolve => setTimeout(resolve, durationSeconds * 1000));
// Restore to normal mode
await fetch(`${baseURL}/channels/${channelId}/normal`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
console.log('[OK] Door restored to normal mode');
return {
channelId,
unlockedFor: durationSeconds,
restoredAt: new Date(),
};
} catch (error) {
console.error('Unlock failed:', error);
throw error;
}
}
// Usage: Unlock front door for 10 seconds
unlockDoor(1340, 10)
.then(result => console.log('Unlock complete:', result));
Warning: unlock mode allows anyone to enter. Prefer admit_person when possible for better security and audit trail.
Web Interface for Security Desk
Allow security desk to grant after-hours access:
// Express.js API endpoint
app.post('/api/after-hours-access', async (req, res) => {
const { personEmail, channelName, reason } = req.body;
try {
// Validate input
if (!personEmail || !channelName || !reason) {
return res.status(400).json({
error: 'Person email, channel name, and reason required',
});
}
// Find person and channel
const person = await findPersonByEmail(personEmail);
const channel = await findChannelByName(channelName);
// Grant access
const result = await grantAfterHoursAccess(person.id, channel.id, reason);
// Log who granted access
await db.insert('after_hours_log', {
person_id: person.id,
channel_id: channel.id,
reason: reason,
granted_by: req.user.id,
granted_by_name: req.user.name,
timestamp: new Date(),
});
res.json({
success: true,
message: `Access granted to ${person.first_name} ${person.last_name} at ${channel.name}`,
person: {
id: person.id,
name: `${person.first_name} ${person.last_name}`,
},
channel: {
id: channel.id,
name: channel.name,
},
});
} catch (error) {
console.error('After-hours access error:', error);
res.status(500).json({
error: 'Failed to grant access',
message: error.message,
});
}
});
Phone Call Workflow
Handle after-hours access requests via phone:
async function handleAfterHoursCall(callerPhone, doorDescription, reason) {
try {
console.log(`After-hours call from: ${callerPhone}`);
console.log(`Requesting access to: ${doorDescription}`);
console.log(`Reason: ${reason}\n`);
// Find person by phone number
const person = await findPersonByPhone(callerPhone);
if (!person) {
console.log('[ERROR] Caller not found in system');
return {
success: false,
message: 'Phone number not registered',
};
}
console.log(`Caller identified: ${person.first_name} ${person.last_name}`);
// Verify person is enabled
if (!person.enabled) {
console.log('[ERROR] Person is disabled (no longer has access)');
return {
success: false,
message: 'Access revoked - contact security',
};
}
// Find the door they're requesting
const channel = await findChannelByName(doorDescription);
console.log(`Door identified: ${channel.name}\n`);
// Grant access
await grantAfterHoursAccess(person.id, channel.id, reason);
// Send SMS confirmation
await smsService.send({
to: callerPhone,
message: `Access granted to ${channel.name}. Please present your credential at the door within 30 seconds.`,
});
console.log('[OK] SMS confirmation sent');
return {
success: true,
person: person,
channel: channel,
};
} catch (error) {
console.error('After-hours call handling failed:', error);
return {
success: false,
message: error.message,
};
}
}
async function findPersonByPhone(phone) {
// Search people by phone number
const result = await db.query(
'SELECT * FROM people WHERE phone = ?',
[phone]
);
return result[0] || null;
}
// Usage
handleAfterHoursCall('+1234567890', 'front door', 'Forgot laptop in office')
.then(result => {
if (result.success) {
console.log('Access granted to:', result.person.first_name);
} else {
console.log('Access denied:', result.message);
}
});
Slack Integration
Grant after-hours access via Slack command:
// Slack command: /grant-access @john front-door Forgot laptop
slackApp.command('/grant-access', async ({ command, ack, respond }) => {
await ack();
try {
// Parse command: @user door-name reason
const parts = command.text.split(' ');
const userMention = parts[0]; // @john
const doorName = parts[1]; // front-door
const reason = parts.slice(2).join(' '); // Forgot laptop
// Get user email from Slack mention
const userId = userMention.replace(/<@|>/g, '');
const slackUser = await slackApp.client.users.info({ user: userId });
const email = slackUser.user.profile.email;
// Grant access
const result = await grantAccessByName(email, doorName, reason);
await respond({
text: `Access granted!`,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*After-Hours Access Granted*\n\nPerson: ${email}\nDoor: ${doorName}\nReason: ${reason}\n\nThey have 30 seconds to present their credential.`,
},
},
],
});
} catch (error) {
await respond({
text: `Failed to grant access: ${error.message}`,
});
}
});
Verify Access Granted
Check if person successfully entered:
async function verifyAccessUsed(personId, channelId, grantedAt) {
const accessToken = process.env.DOORFLOW_ACCESS_TOKEN;
const baseURL = 'https://api.doorflow.com/api/3';
try {
// Wait 1 minute
console.log('Waiting 1 minute to verify access...');
await new Promise(resolve => setTimeout(resolve, 60000));
// Check events for this person at this channel
const eventsResponse = await fetch(
`${baseURL}/events?person_id=${personId}&channel_id=${channelId}&start_time=${grantedAt.toISOString()}`,
{
headers: { 'Authorization': `Bearer ${accessToken}` },
}
);
const { events } = await eventsResponse.json();
// Look for access_granted event
const accessEvent = events.find(e =>
e.event_type === 'access_granted' &&
e.timestamp > grantedAt
);
if (accessEvent) {
console.log('[OK] Person successfully accessed door');
console.log(`Time: ${accessEvent.timestamp}`);
return true;
} else {
console.log('[WARNING] No access event found - person may not have entered');
return false;
}
} catch (error) {
console.error('Verification failed:', error);
return false;
}
}
// Usage
grantAfterHoursAccess(12345, 1340, 'Forgot laptop')
.then(result => {
return verifyAccessUsed(result.personId, result.channelId, result.grantedAt);
})
.then(verified => {
if (verified) {
console.log('Access was used successfully');
} else {
console.log('Access was not used - follow up may be needed');
}
});
Best Practices
1. Always log reason:
// Good: Specific reason
reason: "Forgot laptop in office, retrieving at 10 PM"
// Bad: No context
reason: "Access needed"
2. Verify identity before granting
3. Use admit_person (not unlock):
// Good: admit_person (secure, auditable)
await grantAfterHoursAccess(personId, channelId, reason);
// Bad: unlock (anyone can enter)
// await unlockDoor(channelId);
4. Notify person:
// Send SMS confirmation
await smsService.send({
to: person.phone,
message: `Access granted to ${channel.name}. Present your credential within 30 seconds.`,
});
5. Monitor events:
// Check if access was actually used
setTimeout(async () => {
const used = await verifyAccessUsed(personId, channelId, grantedAt);
if (!used) {
await notifySecurityTeam(`After-hours access granted but not used: Person ${personId}, Channel ${channelId}`);
}
}, 60000); // Check after 1 minute
Difference: admit vs admit_person
admit (unlock for anyone):
POST /api/3/channels/{channel_id}/admit
// Unlocks door once for anyone (no credential needed)
admit_person (unlock for specific person):
POST /api/3/channels/{channel_id}/admit_person
Body: { "person_id": 12345 }
// Unlocks only when this specific person presents credential
Use admit_person for
Use admit for
Required OAuth Scopes
-
account.channel.admit.person- Admit specific person to channel -
account.channel.readonly- List channels (for finding by name) -
account.person.readonly- Find person by email/phone