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
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
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
-
card_lockdown- cards and PINs are rejected. While it is set, the channel'slockdown.messagereads "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.
400 Bad Request.
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:
#!/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:
chmod +x lockdown.sh
./lockdown.sh
JavaScript Version
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
#!/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
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:
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.
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:
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:
// 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:
// 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:
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
5. Document lockdown procedures
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,unlockandnormal