After-Hours Access Workflow

Grant remote access to specific people for single-use door unlock in 3 minutes

3 mins
Beginner

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

Identify the person and door
Use admit_person to unlock for specific person
Log the action
Person has 30 seconds to enter

Time: About 1-2 seconds

When to Use

After-hours access for

Forgotten items (laptop, keys, wallet)
Emergency building entry
One-time access without changing credentials
Remote unlock for specific person
Maintenance after hours

How it works

admit_person unlocks door once for specific person
Person must present their credential within ~30 seconds
Door re-locks automatically
Logged in events as remote admit

Quick Implementation

JavaScript Version

javascript
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

bash
#!/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:

bash
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:

javascript
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):

javascript
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:

javascript
// 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:

javascript
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:

javascript
// 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:

javascript
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:

javascript
// Good: Specific reason
reason: "Forgot laptop in office, retrieving at 10 PM"

// Bad: No context
reason: "Access needed"

2. Verify identity before granting

Confirm phone number matches records
Ask security questions if unsure
Check that person is still employed

3. Use admit_person (not unlock):

javascript
// Good: admit_person (secure, auditable)
await grantAfterHoursAccess(personId, channelId, reason);

// Bad: unlock (anyone can enter)
// await unlockDoor(channelId);

4. Notify person:

javascript
// Send SMS confirmation
await smsService.send({
  to: person.phone,
  message: `Access granted to ${channel.name}. Present your credential within 30 seconds.`,
});

5. Monitor events:

javascript
// 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):

javascript
POST /api/3/channels/{channel_id}/admit
// Unlocks door once for anyone (no credential needed)

admit_person (unlock for specific person):

javascript
POST /api/3/channels/{channel_id}/admit_person
Body: { "person_id": 12345 }
// Unlocks only when this specific person presents credential

Use admit_person for

Better security (specific person only)
Better audit trail (know who accessed)
After-hours access requests

Use admit for

Delivery drivers (don't have credentials)
Emergency access (no time to identify person)
Visitor entry when system is down

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

Next Steps

[Emergency Lockdown] - Lock down all doors
[Visitor Management] - Temporary access with PINs
[Events and Audit Trail] - Monitor after-hours access events

Learn more

[Core Resources] - Understanding channels and people
[Access Control Model] - How permissions work