State Machines

Every Bikeep device has a state that tells you what the device is currently doing. Commands change the state. Understanding this state machine is the key to building a correct integration — it tells you which commands are valid, what to expect, and when to poll for updates.

Device States

State Intuitive Meaning Description
UNLOCKED Available / Ready For docks — the lock arm is open and can be moved freely. For lockers — the locker is physically closed but accepts commands to initiate parking. For doors — the door lock is released (door can be opened).
BOOKED Reserved Reserved for a specific user; will revert to UNLOCKED on timeout
LOCKING Securing Transitioning to locked — command sent, waiting for hardware confirmation
LOCKED In use / Secured For docks — bike is secured by the lock arm. For lockers — locker is actively in use (session in progress). For doors — the door lock is engaged; this is the door’s ready/available state (inverted logic).
UNLOCKING Releasing Transitioning to unlocked — command sent, waiting for hardware confirmation
ALLOCATED Command in flight Device assigned to a user in the backend, awaiting hardware confirmation

Tip: For availability display, prefer the devices.available field from the location endpoint (GET /locations or GET /public-areas/{id}/locations) — it counts devices that are both available AND online, so you don’t need to compute this yourself. If you need per-device logic: for most device types (LOCKER, BIKE_DOCK, SCOOTER_DOCK, etc.), state.value: "UNLOCKED" means available. Note: BIKE_HOUSE_DOOR uses inverted logic — it is available when LOCKED (normally-closed device). GUARD devices are always considered available.

Note: ALLOCATED means the device has been assigned to a user in the backend, but the hardware hasn’t confirmed yet. It typically resolves to LOCKING/BOOKED within seconds, or reverts to UNLOCKED on timeout. The backend attempts automatic sync, but this can fail due to flaky connectivity.

If ALLOCATED persists: the device did not answer, but its latest heartbeat was still fresh enough that the backend allowed the command to be sent. Check heartbeat_v2.health — if DEAD or LONG_DEAD, the device is offline and the command won’t complete. Retry the command once the device is back online, or contact Bikeep support if the state doesn’t resolve.

Note: Maintenance and out-of-service status is tracked at the location level via the location_status field (e.g., MAINTENANCE), not as a device state. See Location Types for location status values.


Command-to-State Transitions

This table shows every valid state transition triggered by a command:

Current State Command Next State Final State Notes
UNLOCKED book BOOKED BOOKED Remains booked until timeout_at or cancellation
BOOKED cancel-booking UNLOCKED UNLOCKED
BOOKED (timeout) UNLOCKED UNLOCKED Automatic — booking expired
BOOKED lock LOCKING LOCKED User checks in to their reserved device
UNLOCKED lock LOCKING LOCKED Direct lock without prior booking
LOCKING unlock UNLOCKING UNLOCKED User cancels before hardware confirms lock
LOCKED unlock UNLOCKING UNLOCKED Ends the session (full lifecycle), device becomes available

Important behaviors

  • lock and book commands initially set the device to ALLOCATED — a brief intermediate state indicating the device has been assigned to a user but hardware hasn’t confirmed yet. ALLOCATED resolves to LOCKING/BOOKED within seconds, or reverts to UNLOCKED on timeout. You do not need to act on ALLOCATED — treat it as “command in progress.”

  • Booking timeout defaults to 1 hour if you omit timeout_at in the book command. After timeout, the device automatically returns to UNLOCKED.

  • LOCKING and UNLOCKING are transient states. These last only as long as it takes for the hardware to confirm (typically 1–5 seconds). You should never need to act on these states — just poll until the device reaches LOCKED or UNLOCKED.


Locker State Diagram

stateDiagram-v2
    [*] --> UNLOCKED

    UNLOCKED --> BOOKED : book
    UNLOCKED --> ALLOCATED : lock
    BOOKED --> ALLOCATED : lock
    BOOKED --> UNLOCKED : cancel-booking / timeout

    ALLOCATED --> LOCKING
    LOCKING --> LOCKED : hardware confirms

    LOCKED --> UNLOCKING : unlock
    UNLOCKING --> UNLOCKED : session ends

    class UNLOCKED available
    class BOOKED reserved
    class ALLOCATED transient
    class LOCKING,LOCKED active
    class UNLOCKING active

Entrance Door (BIKE_HOUSE_DOOR) Behavior

BIKE_HOUSE_DOOR devices alternate between LOCKED and UNLOCKING. They support both RFID card access and the API unlock command:

stateDiagram-v2
    [*] --> LOCKED

    LOCKED --> UNLOCKING : RFID tap or unlock command
    UNLOCKING --> LOCKED : door closes + timeout

    class LOCKED available
    class UNLOCKING active

Inverted logic: For BIKE_HOUSE_DOOR, LOCKED is the ready/idle state (door secured, accepting commands), and UNLOCKING is the active state (door temporarily open). This is the opposite of lockers and docks, where UNLOCKED = ready. When checking availability, a door in LOCKED state is operational and waiting for access requests.

Key difference from lockers: BIKE_HOUSE_DOOR devices only support the unlock command (no lock, book, etc.). Each unlock is a stateless action — no session is created. The door’s RFID access keys are distributed by the parent GUARD (IoT gateway), which manages allowlists for all its children. See Bike House (Dockless) Integration or Bike House (with Docks/Lockers) Integration for details.


Command Lifecycle

Every command you send goes through its own state machine:

stateDiagram-v2
    [*] --> NOT_SENT
    NOT_SENT --> SENT
    SENT --> RECEIVED
    RECEIVED --> SUCCESS
    RECEIVED --> ERROR

    class NOT_SENT,SENT,RECEIVED transient
    class SUCCESS success
    class ERROR error
state.value Meaning
NOT_SENT Command created but not yet dispatched to the device
SENT Command dispatched to the device
RECEIVED Hardware acknowledged receipt
SUCCESS Command executed successfully
ERROR Command failed — check the device state and retry if appropriate

Note: The NOT_SENT → SENT transition is near-instant. The POST response that creates a command will typically already show SENT, so you will almost never observe NOT_SENT in practice. Your polling logic should still handle it — treat it the same as SENT (command in progress).

Note: The command’s state is an object — use state.value to read the current status (e.g., "state": { "value": "SUCCESS" }).

Polling for command completion

After sending a command, poll for its status:

# Send a command
curl -X POST \
  https://services.bikeep.com/device/v1/devices/{device_id}/commands \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{"command": "unlock"}'

# Response includes command id — poll for completion
curl https://services.bikeep.com/device/v1/commands/{command_id} \
  -H "Authorization: Bearer {token}"

Typical round-trip time: 1–5 seconds from SENT to SUCCESS. The backend automatically moves timed-out commands to ERROR, so poll until the command reaches a terminal state (SUCCESS or ERROR). If a command does end in ERROR, check the device’s heartbeat_v2.health to determine whether the device is offline. While the backend handles command timeouts, implementing your own client-side timeout logic is still recommended — it catches edge cases where the device state doesn’t transition as expected (e.g., due to connectivity issues between the devices and the API).

Sleep mode and command latency: Devices with power_save_strategy: REMOTE_WAKEUP in LIGHT_SLEEP may have longer round-trip times while the backend wakes the device — latency normalizes once the device is awake. Devices with power_save_strategy: LOCAL_WAKEUP in DEEP_SLEEP will not respond to commands at all — the command will time out to ERROR. Check sleep_state and power_save_strategy before sending commands to these devices. See Device Types — Power Save and Sleep for details.

Command state.timeout_at: While a command is in SENT state, state.timeout_at indicates the deadline for the command to reach the device. If the device doesn’t acknowledge the command by this time, the backend moves the command to ERROR. On terminal states (SUCCESS or ERROR), state.timeout_at is null.


RFID-Triggered Transitions

When a whitelisted RFID card is tapped on a device, the hardware triggers state transitions automatically without an API command:

Device State RFID Tap Result
UNLOCKED Whitelisted card Device locks → LOCKED (session starts, tied to that card)
LOCKED Same card that locked it Device unlocks → UNLOCKED (session ends)
LOCKED Card with is_master_key: true Device unlocks → UNLOCKED (session ends)
LOCKED Different non-master card No effect (access denied)

These RFID-triggered transitions generate the same state changes you would see via the API. Monitor them by polling the device endpoint or the location endpoint for aggregated availability.

Note: RFID taps cannot trigger or consume bookings. The system cannot infer intent from a physical card tap — booking is designed for remote reservation of parking places via the API. A BOOKED device will not respond to RFID taps; the booking must be consumed or cancelled through the API.


Hardware States

Separate from the logical device state, each device reports a hardware_state:

Hardware State Meaning
NORMAL Hardware operating correctly
DISABLED Hardware administratively disabled
DISABLED_LOCKING Device can only accept unlock commands (locking disabled)
DISABLED_UNLOCKING Device can only accept lock commands (unlocking disabled)
ERROR Hardware error detected
NOT_RESPONDING Hardware not responding to commands (legacy G1 docks only)

In practice, only NORMAL and NOT_RESPONDING are actively used. NOT_RESPONDING applies to legacy G1 docks only. The DISABLED_* and ERROR states exist in the API schema but are rarely encountered in production.

Heartbeat Health (heartbeat_v2.health)

Use heartbeat_v2.health to determine whether a device is online before sending commands:

Health Meaning
HEALTHY Device is communicating normally
UNHEALTHY Heartbeat delayed (~15 seconds without heartbeat)
DEAD No recent heartbeat (~30 seconds) — device may be offline
LONG_DEAD Device offline for 2+ hours

Legacy field: The deprecated heartbeat_health field still exists but maps LONG_DEAD to DEAD (losing the distinction). Use heartbeat_v2.health for new integrations. See Device Types — Common Properties for the full heartbeat_v2 object.

Use hardware_state and heartbeat_v2.health together to determine if a device is operational before sending commands.