Visitor Management Workflow

Grant temporary access to visitors with time-limited PINs in 7 minutes

7 mins
Intermediate

Complete code to check in visitors with temporary access. Auto-generated PINs sent via email/SMS with automatic expiration.

The Workflow

Scenario: Visitor arriving at 9 AM, meeting until 5 PM, needs lobby and conference room access.

What we'll do

Create visitor person record
Issue auto-generated PIN
Create time-limited reservation
Send PIN to visitor
Access automatically expires

Time: About 2-3 seconds per visitor

Prerequisites

You need

OAuth access token with scopes: account.person, account.person, account.reservation
Visitor group ID (create one if you don't have)
Channel IDs for doors visitor can access
Email/SMS service to send PIN

Get your channel IDs:

bash
curl -X GET "https://api.doorflow.com/api/3/channels" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Quick Visitor Check-in

Bash Script

Complete script for command-line visitor check-in:

bash
#!/bin/bash
# Visitor Check-in Script
# Usage: ./checkin_visitor.sh "Jane" "Smith" "jane@example.com" "1340,1341" "8"

FIRST_NAME=$1
LAST_NAME=$2
EMAIL=$3
CHANNEL_IDS=$4  # Comma-separated
HOURS=$5        # How many hours from now

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

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

echo "Checking in visitor: $FIRST_NAME $LAST_NAME"
echo "Access until: $END_TIME"

# Step 1: Create visitor (assign to Visitors group ID 2)
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\": [2]
  }")

PERSON_ID=$(echo $PERSON_RESPONSE | jq -r '.id')

if [ "$PERSON_ID" == "null" ]; then
  echo "Error creating visitor:"
  echo $PERSON_RESPONSE | jq '.'
  exit 1
fi

echo "[OK] Visitor created (ID: $PERSON_ID)"

# Step 2: Issue auto-generated 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"

# Step 3: Create reservation
RESERVATION_RESPONSE=$(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\"
  }")

RESERVATION_ID=$(echo $RESERVATION_RESPONSE | jq -r '.id')
echo "[OK] Reservation created (ID: $RESERVATION_ID)"

echo ""
echo "[OK] Visitor check-in complete!"
echo ""
echo "Visitor Information:"
echo "  Name: $FIRST_NAME $LAST_NAME"
echo "  Email: $EMAIL"
echo "  PIN: $PIN_VALUE"
echo "  Access Until: $END_TIME"
echo ""
echo "Next steps:"
echo "  1. Send PIN to visitor: $PIN_VALUE"
echo "  2. Provide door access instructions"

Save as: checkin_visitor.sh

Run:

bash
chmod +x checkin_visitor.sh
./checkin_visitor.sh "Jane" "Smith" "jane@example.com" "1340,1341" "8"

JavaScript Version with Email

Complete implementation with automatic email notification:

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

  // Calculate time window (now to now + hours)
  const now = new Date();
  const startTime = new Date(now);
  const endTime = new Date(now.getTime() + (hours * 60 * 60 * 1000));

  try {
    console.log(`Checking in visitor: ${firstName} ${lastName}`);
    console.log(`Access: ${startTime.toISOString()} to ${endTime.toISOString()}`);

    // Step 1: Create visitor
    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: [2], // Visitors group
      }),
    });

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

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

    // Step 2: Issue auto-generated 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,
        },
      }),
    });

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

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

    // Step 3: Create time-limited reservation
    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: startTime.toISOString(),
        end_time: endTime.toISOString(),
      }),
    });

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

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

    // Step 4: Send email to visitor
    console.log('Sending access information to visitor...');

    await sendVisitorEmail(email, {
      firstName: firstName,
      lastName: lastName,
      pin: pin.value,
      expiresAt: endTime,
    });

    console.log('[OK] Visitor checked in successfully!');

    return {
      personId: person.id,
      firstName: firstName,
      lastName: lastName,
      email: email,
      pin: pin.value,
      reservationId: reservation.id,
      expiresAt: endTime,
    };

  } catch (error) {
    console.error('Visitor check-in failed:', error);
    throw error;
  }
}

async function sendVisitorEmail(email, details) {
  // Integrate with your email service (SendGrid, AWS SES, etc.)
  const emailBody = `
    Dear ${details.firstName} ${details.lastName},

    Welcome! Here is your temporary building access information:

    Access PIN: ${details.pin}

    Your access is valid until: ${details.expiresAt.toLocaleString()}

    Instructions:
    1. Enter the PIN on the keypad at the entrance
    2. Press # after entering the PIN
    3. Wait for the green light and door unlock

    If you have any issues, please contact reception.

    This PIN will automatically expire after your visit.

    Thank you for visiting!
  `;

  console.log(`Sending PIN to ${email}: ${details.pin}`);

  // Example: SendGrid
  // await sendgrid.send({
  //   to: email,
  //   from: 'reception@yourcompany.com',
  //   subject: 'Your Building Access PIN',
  //   text: emailBody,
  // });

  // Example: AWS SES
  // await ses.sendEmail({
  //   Destination: { ToAddresses: [email] },
  //   Message: {
  //     Subject: { Data: 'Your Building Access PIN' },
  //     Body: { Text: { Data: emailBody } },
  //   },
  //   Source: 'reception@yourcompany.com',
  // }).promise();
}

// Usage
checkInVisitor('Jane', 'Smith', 'jane@example.com', [1340, 1341], 8)
  .then(result => {
    console.log('\nVisitor check-in complete:', result);
    console.log(`\nPIN: ${result.pin} (valid until ${result.expiresAt.toLocaleString()})`);
  })
  .catch(error => console.error('Error:', error.message));

Scheduled Visitor Pre-registration

Register visitors in advance for future visits:

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

  // Visit date at 9 AM
  const startTime = new Date(visitDate);
  startTime.setHours(9, 0, 0, 0);

  // End time = start + visitHours
  const endTime = new Date(startTime.getTime() + (visitHours * 60 * 60 * 1000));

  try {
    // Create visitor (disabled until visit day)
    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: false, // Disabled until visit day
        group_ids: [2],
      }),
    });

    const person = await personResponse.json();
    console.log(`[OK] Visitor pre-registered (ID: ${person.id})`);

    // Issue PIN (but person is disabled)
    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] PIN issued: ${pin.value}`);

    // Create future reservation
    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: startTime.toISOString(),
        end_time: endTime.toISOString(),
      }),
    });

    const reservation = await reservationResponse.json();

    // Enable person (now PIN will work during reservation window)
    await fetch(`${baseURL}/people/${person.id}`, {
      method: 'PUT',
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        enabled: true,
      }),
    });

    console.log(`[OK] Visitor access scheduled for ${startTime.toLocaleDateString()}`);

    // Send email with PIN
    await sendVisitorEmail(email, {
      firstName: firstName,
      lastName: lastName,
      pin: pin.value,
      expiresAt: endTime,
    });

    return {
      personId: person.id,
      pin: pin.value,
      visitDate: startTime,
      expiresAt: endTime,
    };

  } catch (error) {
    console.error('Visitor pre-registration failed:', error);
    throw error;
  }
}

// Usage: Register visitor for next Monday
const nextMonday = new Date('2024-06-10');
preRegisterVisitor('Bob', 'Johnson', 'bob@example.com', [1340, 1341], nextMonday, 8)
  .then(result => console.log('Visitor pre-registered:', result));

Bulk Visitor Check-in

Check in multiple visitors at once:

javascript
async function bulkCheckInVisitors(visitors, channelIds, hours = 8) {
  console.log(`Checking in ${visitors.length} visitors...`);

  const results = [];

  for (const visitor of visitors) {
    try {
      const result = await checkInVisitor(
        visitor.firstName,
        visitor.lastName,
        visitor.email,
        channelIds,
        hours
      );

      results.push({ success: true, ...result });

      // Delay to avoid rate limits
      await new Promise(resolve => setTimeout(resolve, 2000));

    } catch (error) {
      console.error(`Failed to check in ${visitor.firstName} ${visitor.lastName}:`, error.message);
      results.push({
        success: false,
        firstName: visitor.firstName,
        lastName: visitor.lastName,
        email: visitor.email,
        error: error.message,
      });
    }
  }

  const successful = results.filter(r => r.success);
  const failed = results.filter(r => !r.success);

  console.log(`\n[OK] Bulk check-in complete`);
  console.log(`Successful: ${successful.length}`);
  console.log(`Failed: ${failed.length}`);

  return results;
}

// Usage
const visitors = [
  { firstName: 'Jane', lastName: 'Smith', email: 'jane@example.com' },
  { firstName: 'Bob', lastName: 'Jones', email: 'bob@example.com' },
  { firstName: 'Alice', lastName: 'Brown', email: 'alice@example.com' },
];

bulkCheckInVisitors(visitors, [1340, 1341], 8)
  .then(results => console.log('Results:', results));

Visitor Check-out

Optionally revoke access early when visitor leaves:

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

  try {
    // Disable person (revokes access immediately)
    await fetch(`${baseURL}/people/${personId}`, {
      method: 'PUT',
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        enabled: false,
      }),
    });

    console.log(`[OK] Visitor checked out (person ${personId})`);
    console.log('[OK] Access revoked');

    return {
      personId: personId,
      checkedOutAt: new Date(),
    };

  } catch (error) {
    console.error('Visitor check-out failed:', error);
    throw error;
  }
}

// Usage
checkOutVisitor(12345)
  .then(result => console.log('Visitor checked out:', result));

Reception Desk Integration

Web interface for reception to check in visitors:

javascript
// Express.js API endpoint
app.post('/api/visitors/checkin', async (req, res) => {
  const { firstName, lastName, email, channelIds, hours } = req.body;

  try {
    // Validate input
    if (!firstName || !lastName || !email) {
      return res.status(400).json({
        error: 'First name, last name, and email required',
      });
    }

    // Check in visitor
    const result = await checkInVisitor(
      firstName,
      lastName,
      email,
      channelIds || [1340, 1341], // Default channels
      hours || 8
    );

    // Log check-in
    await db.insert('visitor_log', {
      person_id: result.personId,
      checked_in_at: new Date(),
      checked_in_by: req.user.id,
      pin: encrypt(result.pin), // Encrypt in logs
    });

    res.json({
      success: true,
      visitor: result,
      message: `Visitor checked in. PIN sent to ${email}`,
    });

  } catch (error) {
    console.error('Check-in error:', error);
    res.status(500).json({
      error: 'Check-in failed',
      message: error.message,
    });
  }
});

Best Practices

1. Use reservations for automatic expiration

Reservation end time = automatic access revocation
No manual check-out needed
Maintains clean audit trail

2. Auto-generate PINs:

javascript
// Always use "******" for random PIN
value: "******"  // Good

// Never use predictable PINs
value: "1234"    // Bad

3. Send PIN via multiple channels:

javascript
// Email AND SMS for reliability
await sendVisitorEmail(email, details);
await sendVisitorSMS(phone, details.pin);

4. Limit channel access:

javascript
// Only grant access to necessary doors
channelIds: [1340, 1341]  // Lobby + Conference Room

// Don't grant access to all doors
// channelIds: allChannels  // Bad - too much access

5. Clean up old visitors:

javascript
// Cron job to disable expired visitors (daily)
async function cleanupExpiredVisitors() {
  const yesterday = new Date();
  yesterday.setDate(yesterday.getDate() - 1);

  // Find visitors with expired reservations
  const expiredReservations = await db.query(
    'SELECT person_id FROM reservations WHERE end_time < ?',
    [yesterday]
  );

  for (const row of expiredReservations) {
    // Disable visitor
    await offboardEmployee(row.person_id);
  }
}

Required OAuth Scopes

  • account.person - Create visitor
  • account.person - Issue PIN
  • account.reservation - Create time-limited access

Next Steps

[Employee Onboarding] - Permanent access for employees
[Contractor Access] - Longer-term temporary access
[After-Hours Access] - Remote unlock for specific people

Learn more

[PIN Credentials] - Details about PINs
[Events and Audit Trail] - Monitor visitor access