Emergency Lockdown Workflow

Request a lockdown during a security incident, check the API state, and end it

3 mins
Beginner

Request a lockdown during a security incident, check which requests the API accepted, and end the lockdown afterwards.

The Workflow

Scenario: Security incident. You need to stop entry to the building now.

What we'll do

•
Get all channels (doors)
•
Request a lockdown for each channel
•
Check the result of every request, and list any request that failed

Time: Request time depends on the number of doors. API success does not measure when controllers apply the change.

When to Use

Emergency lockdown for

•
Active security threat
•
After-hours security
•
Testing emergency procedures

Not for a fire alarm or an evacuation. A lockdown is an entry control, and it is not a substitute for fire-rated release hardware. See Lockdown is not for fire or evacuation.

How it works

•
A lockdown request sets up to three flags on a channel:
  • card_lockdown - cards and PINs are rejected. While it is set, the channel's lockdown.message reads "Cards and PINs will be rejected".
  • rex_lockdown - locks down REX (Request to Exit).
  • aux_lockdown - locks down the AUX inputs.
•
card_lockdown - cards and PINs are rejected. While it is set, the channel's lockdown.message reads "Cards and PINs will be rejected".
•
rex_lockdown - locks down REX (Request to Exit).
•
aux_lockdown - locks down the AUX inputs.
•
At least one flag must be true, or the request fails with 400 Bad Request.
•
While a channel is in lockdown, a remote admit through the API is refused.
•
The door stays in lockdown until you end it with unlockdown.

The API records the requested mode and queues its command. It can return 200 OK while the controller is offline; the mode is replayed when the controller reconnects. A successful response or the channel’s lockdown field confirms the recorded mode, not that the physical door has applied it. Verify the doors through your operational monitoring or on site before relying on the lockdown.

Which flags you set decides what still works at the door. The examples below set card_lockdown only, and leave REX and AUX alone. Set rex_lockdown or aux_lockdown only when you mean to lock those down too.

Lockdown is not for fire or evacuation

Do not use a lockdown for a fire alarm or an evacuation. DoorFlow's knowledge base is clear that fire release is hardware's job:

  • Article 47926 (fire alarm): "The access control system should not prevent exit in the case of a fire." It sets out the release options: a fire-panel dry-contact relay, a green break glass, a release signal over fire-rated cabling, and mechanical exit hardware (BS EN 179).
  • Article 47973: the fire relay and green break glass cut lock power, outside DoorFlow's control.
  • Article 48023: DoorFlow "doesn't override or remotely control hardware wiring."

DoorFlow has no fire-alarm input, and the API has no evacuation mode. A lockdown restricts entry; it does nothing to release a door.

Quick Lockdown Script

The locked and ended counters in these examples count accepted API requests, not hardware acknowledgements.

Bash Version

This script needs curl and jq:

bash
#!/bin/bash
# Emergency lockdown script
# Usage: ./lockdown.sh

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

echo "INITIATING EMERGENCY LOCKDOWN..."

# The channel index is a JSON array. skip_pagination returns every channel,
# not only the first page of 500.
CHANNELS=$(curl -s -f "$BASE_URL/channels?skip_pagination=true" \
  -H "Authorization: Bearer $ACCESS_TOKEN") || { echo "[ERROR] Could not list channels"; exit 1; }

CHANNEL_IDS=$(printf '%s' "$CHANNELS" | jq -r '
  if type == "array" and all(.[]; (.id | type) == "number")
  then .[].id else error("Expected a channel array with numeric IDs") end
') || { echo "[ERROR] Invalid channel list"; exit 1; }

FAILED=0
for CHANNEL_ID in $CHANNEL_IDS; do
  CHANNEL_NAME=$(echo "$CHANNELS" | jq -r ".[] | select(.id==$CHANNEL_ID) | .name")

  STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BASE_URL/channel/$CHANNEL_ID/lockdown" \
    -H "Authorization: Bearer $ACCESS_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"card_lockdown": true, "rex_lockdown": false, "aux_lockdown": false}')

  if [ "$STATUS" = "200" ]; then
    echo "[OK] $CHANNEL_NAME (ID: $CHANNEL_ID): lockdown request accepted"
  else
    echo "[FAILED] $CHANNEL_NAME (ID: $CHANNEL_ID): HTTP $STATUS"
    FAILED=$((FAILED + 1))
  fi
done

if [ "$FAILED" -gt 0 ]; then
  echo "LOCKDOWN REQUESTS INCOMPLETE: $FAILED request(s) failed"
  exit 1
fi

echo "LOCKDOWN REQUESTS ACCEPTED"
echo "To end it: ./end_lockdown.sh"

Save as: lockdown.sh

Run:

bash
chmod +x lockdown.sh
./lockdown.sh

JavaScript Version

javascript
const LOCKDOWN_FLAGS = { card_lockdown: true, rex_lockdown: false, aux_lockdown: false };

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

  console.log('INITIATING EMERGENCY LOCKDOWN...\n');

  // Step 1: Get all channels. The index is a JSON array.
  const channelsResponse = await fetch(`${baseURL}/channels?skip_pagination=true`, {
    headers: { 'Authorization': `Bearer ${accessToken}` },
  });

  if (!channelsResponse.ok) {
    throw new Error(`Failed to get channels: HTTP ${channelsResponse.status}`);
  }

  const channels = await channelsResponse.json();
  console.log(`Locking down ${channels.length} doors...\n`);

  // Step 2: Lock down each channel, recording the status of every request
  const results = [];

  for (const channel of channels) {
    try {
      const lockdownResponse = await fetch(`${baseURL}/channel/${channel.id}/lockdown`, {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${accessToken}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(LOCKDOWN_FLAGS),
      });

      const success = lockdownResponse.status === 200;
      console.log(success
        ? `[OK] ${channel.name}: lockdown request accepted`
        : `[FAILED] ${channel.name}: HTTP ${lockdownResponse.status}`);
      results.push({ channelId: channel.id, name: channel.name, success, status: lockdownResponse.status });
    } catch (error) {
      console.error(`[FAILED] ${channel.name}: ${error.message}`);
      results.push({ channelId: channel.id, name: channel.name, success: false, error: error.message });
    }
  }

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

  console.log(`\nLockdown requests accepted: ${successful.length}`);
  console.log(`Failed: ${failed.length}`);
  failed.forEach(f => console.log(`  - ${f.name} (ID: ${f.channelId})`));

  return {
    totalDoors: channels.length,
    locked: successful.length,
    failed: failed.length,
    results,
  };
}

// Usage
emergencyLockdown()
  .then(result => console.log('\nLockdown result:', result))
  .catch(error => console.error('LOCKDOWN FAILED:', error.message));

End the Lockdown

End a lockdown with unlockdown. It clears all three lockdown flags. If the door still has an unlock that has not expired, ending the lockdown resumes it.

Bash Version

bash
#!/bin/bash
# End the lockdown
# Usage: ./end_lockdown.sh

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

CHANNELS=$(curl -s -f "$BASE_URL/channels?skip_pagination=true" \
  -H "Authorization: Bearer $ACCESS_TOKEN") || { echo "[ERROR] Could not list channels"; exit 1; }

# Only the channels that are in lockdown: their "lockdown" key is not null.
CHANNEL_IDS=$(printf '%s' "$CHANNELS" | jq -r '
  if type == "array" and all(.[]; (.id | type) == "number")
  then .[] | select(.lockdown != null) | .id
  else error("Expected a channel array with numeric IDs") end
') || { echo "[ERROR] Invalid channel list"; exit 1; }

FAILED=0
for CHANNEL_ID in $CHANNEL_IDS; do
  CHANNEL_NAME=$(echo "$CHANNELS" | jq -r ".[] | select(.id==$CHANNEL_ID) | .name")

  STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BASE_URL/channel/$CHANNEL_ID/unlockdown" \
    -H "Authorization: Bearer $ACCESS_TOKEN")

  if [ "$STATUS" = "200" ]; then
    echo "[OK] $CHANNEL_NAME: end-lockdown request accepted"
  else
    echo "[FAILED] $CHANNEL_NAME: HTTP $STATUS"
    FAILED=$((FAILED + 1))
  fi
done

[ "$FAILED" -eq 0 ] || { echo "$FAILED end-lockdown request(s) failed"; exit 1; }
echo "[OK] End-lockdown requests accepted"

JavaScript Version

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

  const channelsResponse = await fetch(`${baseURL}/channels?skip_pagination=true`, {
    headers: { 'Authorization': `Bearer ${accessToken}` },
  });

  if (!channelsResponse.ok) {
    throw new Error(`Failed to get channels: HTTP ${channelsResponse.status}`);
  }

  const channels = await channelsResponse.json();
  const lockedDown = channels.filter(channel => channel.lockdown !== null);
  const failed = [];

  for (const channel of lockedDown) {
    const response = await fetch(`${baseURL}/channel/${channel.id}/unlockdown`, {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${accessToken}` },
    });

    if (response.status === 200) {
      console.log(`[OK] ${channel.name}: end-lockdown request accepted`);
    } else {
      console.log(`[FAILED] ${channel.name}: HTTP ${response.status}`);
      failed.push(channel);
    }
  }

  return {
    lockedDown: lockedDown.length,
    ended: lockedDown.length - failed.length,
    failed: failed.length,
  };
}

// Usage
endLockdown()
  .then(result => console.log('End lockdown result:', result))
  .catch(error => console.error('Error:', error.message));

Selective Lockdown

Lock down specific doors only (for example, one floor). The lockdown response is the channel itself, so it carries the door's name:

javascript
async function selectiveLockdown(channelIds) {
  const accessToken = process.env.DOORFLOW_ACCESS_TOKEN;
  const baseURL = 'https://api.doorflow.com/api/3';
  const failed = [];

  for (const channelId of channelIds) {
    const response = await fetch(`${baseURL}/channel/${channelId}/lockdown`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ card_lockdown: true, rex_lockdown: false, aux_lockdown: false }),
    });

    if (response.status === 200) {
      const channel = await response.json();
      console.log(`[OK] ${channel.name}: lockdown request accepted`);
    } else {
      console.log(`[FAILED] channel ${channelId}: HTTP ${response.status}`);
      failed.push(channelId);
    }
  }

  return { requested: channelIds.length, failed };
}

// Usage: lock down only floors 2 and 3
const floor2And3Doors = [1340, 1341, 1342, 1343];
selectiveLockdown(floor2And3Doors)
  .then(result => console.log('Selective lockdown result:', result));

Lockdown Status Check

This checks the mode recorded by the API, not physical-door confirmation.

A channel's lockdown state is its lockdown key: null when the channel is not in lockdown, otherwise an object holding the state of each flag. mode.description is only ever unlock or normal, so it cannot tell you about a lockdown.

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

  const channelsResponse = await fetch(`${baseURL}/channels?skip_pagination=true`, {
    headers: { 'Authorization': `Bearer ${accessToken}` },
  });

  if (!channelsResponse.ok) {
    throw new Error(`Failed to get channels: HTTP ${channelsResponse.status}`);
  }

  const channels = await channelsResponse.json();
  const lockdownDoors = channels.filter(channel => channel.lockdown !== null);
  const otherDoors = channels.filter(channel => channel.lockdown === null);

  console.log(`Channels with a recorded lockdown: ${lockdownDoors.length}`);
  console.log(`Channels without a recorded lockdown: ${otherDoors.length}`);

  lockdownDoors.forEach(channel => {
    const { card_lockdown, rex_lockdown, aux_lockdown } = channel.lockdown;
    console.log(`  - ${channel.name} (ID: ${channel.id}): cards ${card_lockdown.state}, REX ${rex_lockdown.state}, AUX ${aux_lockdown.state}`);
  });

  return {
    totalDoors: channels.length,
    lockdownDoors: lockdownDoors.length,
    otherDoors: otherDoors.length,
    lockedDownList: lockdownDoors,
  };
}

// Usage
checkLockdownStatus()
  .then(result => console.log('Status:', result));

Lockdown with Notification

Send alerts when lockdown requests have been processed:

javascript
async function lockdownWithAlert(reason) {
  console.log(`EMERGENCY LOCKDOWN: ${reason}\n`);

  // Lock down all doors
  const result = await emergencyLockdown();

  // Log the incident
  await db.insert('security_incidents', {
    type: 'lockdown',
    reason: reason,
    initiated_by: currentUser.id,
    initiated_at: new Date(),
    doors_locked: result.locked,
    doors_failed: result.failed,
  });

  // Send alerts
  await sendEmergencyAlerts({
    reason: reason,
    doorsLocked: result.locked,
    doorsFailed: result.failed,
    time: new Date(),
  });

  console.log('[OK] Emergency alerts sent');

  return result;
}

async function sendEmergencyAlerts(details) {
  // Email security team
  await emailService.send({
    to: ['security@company.com', 'facilities@company.com'],
    subject: `EMERGENCY: Building Lockdown Requested`,
    body: `
      Emergency lockdown has been requested.

      Reason: ${details.reason}
      Time: ${details.time.toLocaleString()}
      Lockdown requests accepted: ${details.doorsLocked}
      Doors that failed: ${details.doorsFailed}
    `,
    priority: 'high',
  });

  // SMS security team
  await smsService.send({
    to: ['+1234567890', '+0987654321'],
    message: `EMERGENCY LOCKDOWN: ${details.reason}. ${details.doorsLocked} lockdown requests accepted, ${details.doorsFailed} failed, at ${details.time.toLocaleTimeString()}`,
  });

  // Post to Slack security channel
  await slackApp.client.chat.postMessage({
    channel: '#security-alerts',
    text: `EMERGENCY LOCKDOWN REQUESTED`,
    blocks: [
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: `*EMERGENCY: Building Lockdown*\n\nReason: ${details.reason}\nTime: ${details.time.toLocaleString()}\nLockdown requests accepted: ${details.doorsLocked}\nFailed: ${details.doorsFailed}`,
        },
      },
    ],
  });
}

// Usage
lockdownWithAlert('Active security threat in building')
  .then(result => console.log('Lockdown requests processed and alerts sent'));

Best Practices

1. Test lockdown regularly:

javascript
// Monthly lockdown test
async function testLockdown() {
  console.log('LOCKDOWN TEST - This is a drill\n');

  await emergencyLockdown();

  // Wait 30 seconds
  await new Promise(resolve => setTimeout(resolve, 30000));

  await endLockdown();

  console.log('LOCKDOWN TEST COMPLETE');
}

2. Log all lockdowns:

javascript
// Always log lockdown events
await db.insert('lockdown_log', {
  initiated_by: currentUser.id,
  reason: reason,
  doors_locked: result.locked,
  timestamp: new Date(),
});

3. Check the recorded API mode, then verify the doors operationally:

javascript
async function verifyLockdown() {
  const status = await checkLockdownStatus();

  if (status.otherDoors > 0) {
    console.log('[WARNING] Some channels have no recorded lockdown!');
    return false;
  }

  console.log('[OK] All channels have a recorded lockdown; verify the doors operationally');
  return true;
}

4. Have a manual override process

•
Physical key override at doors
•
Don't rely solely on the API

5. Document lockdown procedures

•
Who can initiate lockdown
•
When to use lockdown, and that fire and evacuation are handled by the building's fire-rated release hardware, not by a lockdown
•
How to end a lockdown
•
Emergency contact numbers

Channel Commands Reference

Channel commands are sent to the singular /channel/{channel_id} path:

Request What it does
POST /api/3/channel/{channel_id}/lockdown Starts a lockdown. Send at least one of card_lockdown, rex_lockdown and aux_lockdown as true, or it fails with 400 Bad Request
POST /api/3/channel/{channel_id}/unlockdown Ends a lockdown, clearing all three flags
POST /api/3/channel/{channel_id}/unlock Holds the door unlocked, until relock_at if you send one, otherwise until midnight
POST /api/3/channel/{channel_id}/normal Cancels an unlock. It does not end a lockdown, and it fails with 400 Bad Request when there is no unlock to cancel

Required OAuth Scopes

  • account.channel.readonly - list and read channels
  • account.channel.control - lockdown, unlockdown, unlock and normal

Next Steps

•
[After-Hours Access] - Remote unlock for specific people
•
[Events and Audit Trail] - Monitor lockdown events

Learn more

•
[Core Resources] - Understanding channels
•
[Access Control Model] - How access works