API Reference¶
The OpenLatch backend exposes two API groups: - Admin API — for the web UI and admin tools (JWT-authenticated) - Device API — for ESP32 devices (bearer device-token)
Base URL: https://<your-server>/api/v1
Authentication¶
Admin API¶
Uses JWT bearer tokens. Obtain a token via login:
POST /api/v1/auth/login
Content-Type: application/json
{
"username": "admin",
"password": "secret"
}
Response:
Use the token in subsequent requests:
Tokens expire after 15 minutes. Refresh with:
Device API¶
Uses a pre-shared device token, provisioned during ESP32 setup:
Admin API Endpoints¶
Members¶
List Members¶
Query parameters:
- role (optional): filter by role (admin, keyholder, member, guest)
- is_active (optional): true or false
Response:
[
{
"id": "uuid",
"username": "alice",
"email": "alice@example.com",
"display_name": "Alice",
"role": "keyholder",
"is_active": true,
"created_at": "2026-01-15T10:30:00Z"
}
]
Create Member¶
POST /api/v1/members
Content-Type: application/json
{
"username": "bob",
"email": "bob@example.com",
"password": "initial-password",
"display_name": "Bob",
"role": "member"
}
Get Member¶
Update Member¶
PUT /api/v1/members/{id}
Content-Type: application/json
{
"display_name": "Bob Smith",
"role": "keyholder"
}
Deactivate Member¶
Soft-delete. Revokes all associated NFC keys.
NFC Keys¶
List Keys for a Member¶
Register Key¶
POST /api/v1/nfc-keys
Content-Type: application/json
{
"member_id": "uuid",
"uid": "AABBCCDD",
"label": "Blue keychain tag",
"key_type": "mifare_desfire",
"expires_at": "2026-12-31T23:59:59Z"
}
uid is the hex-encoded NFC UID. expires_at is optional (null = no expiry).
Revoke Key¶
Sets revoked_at timestamp. The key is excluded from the next allow-list generation.
Access Rules¶
List Rules¶
Create Rule¶
POST /api/v1/access-rules
Content-Type: application/json
{
"name": "Weekday office hours",
"rule_type": "time_window",
"parameters": {
"days": ["mon", "tue", "wed", "thu", "fri"],
"start_hour": 8,
"start_minute": 0,
"end_hour": 22,
"end_minute": 0,
"timezone": "Europe/Berlin"
},
"priority": 10,
"targets": [
{"target_type": "role", "target_value": "member"}
]
}
Rule Types and Parameters¶
time_window — restrict access to specific hours on specific days:
{
"days": ["mon", "tue", "wed", "thu", "fri"],
"start_hour": 8, "start_minute": 0,
"end_hour": 22, "end_minute": 0,
"timezone": "Europe/Berlin"
}
date_range — temporary access for a date range:
{
"start_date": "2026-03-01T15:00:00+01:00",
"end_date": "2026-03-03T18:00:00+01:00",
"grace_minutes": 15
}
guest_sponsor — guest access sponsored by a keyholder:
role_minimum — minimum role required:
global_override — emergency controls:
or:
Update Rule¶
PUT /api/v1/access-rules/{id}
Content-Type: application/json
{
"name": "Updated name",
"is_active": false
}
Delete Rule¶
Audit Log¶
Query Audit Log¶
Query parameters:
- event_type (optional): filter by event type
- actor_id (optional): filter by who performed the action
- since (optional): ISO 8601 timestamp
- limit (optional): max results (default 100)
Response:
[
{
"id": 42,
"event_type": "key_revoked",
"actor_id": "admin-uuid",
"target_id": "key-uuid",
"details": {"reason": "Card lost"},
"created_at": "2026-02-26T14:30:00Z"
}
]
Event types: member_created, member_deactivated, key_registered, key_revoked, rule_created, rule_updated, rule_deleted, access_granted, access_denied, emergency_access, anomaly_detected, locked_by_button, allowlist_generated, nuki_daily_report, nuki_battery_low.
Health¶
Response:
Monitoring¶
OpenLatch provides two monitoring endpoints (no authentication required):
GET /api/v1/monitoring/nuki-battery— Nagios/Icinga check (plain text with perfdata)GET /api/v1/monitoring/metrics— Prometheus exposition format
See Monitoring for endpoint details and setup guides for Icinga2, Nagios, Checkmk, Zabbix, Prometheus, and Uptime Kuma.
Device API Endpoints¶
Fetch Allow-List¶
Responses:
- 200 OK with Content-Type: application/octet-stream — new allow-list (binary OL02 payload)
- 304 Not Modified — ESP32's version is current
Report Events¶
Sent during each sync cycle with any queued events.
POST /api/v1/device/events
Authorization: Bearer <device-token>
Content-Type: application/json
[
{
"uid": "AABBCCDD",
"event": "access_granted",
"timestamp": 1700000000
},
{
"uid": "EEFF0011",
"event": "emergency_access",
"timestamp": 1700000100
}
]
Event types from device: access_granted, access_denied, access_grace, lock_only, emergency_access, anomaly_outside_hours, conditional_approved, conditional_denied, conditional_timeout, locked_by_button, locked_auto, nuki_daily_report, nuki_battery_low.
Request Conditional Approval¶
When a CONDITIONAL key is scanned and network is available:
POST /api/v1/device/approve
Authorization: Bearer <device-token>
Content-Type: application/json
{
"uid": "AABBCCDD",
"timestamp": 1700000000
}
Response (blocking, up to 60 seconds):
or:
or:
Heartbeat¶
POST /api/v1/device/heartbeat
Authorization: Bearer <device-token>
Content-Type: application/json
{
"firmware_version": "0.1.0",
"allowlist_version": 42,
"uptime_seconds": 86400,
"free_heap": 120000,
"wifi_rssi": -55,
"nuki_battery_percent": 87,
"nuki_battery_critical": false,
"nuki_paired": true,
"nuki_state": "locked"
}
NUKI fields (all optional, omitted if no NUKI lock is configured):
| Field | Type | Description |
|---|---|---|
nuki_battery_percent |
int | Cached battery level (0-100), -1 if unknown |
nuki_battery_critical |
bool | NUKI's critical battery flag |
nuki_paired |
bool | Whether the ESP32 is paired with a NUKI lock |
nuki_state |
string | Current lock state: locked, unlocked, unlatched, unlatching, unlocking, locking, motor_blocked, uncalibrated, boot_run, unknown |
NUKI Device Events¶
nuki_daily_report — sent once per day at 3am (includes fresh battery from standalone BLE query):
{
"uid": "",
"event": "nuki_daily_report",
"timestamp": 1700000000,
"details": {
"battery_percent": 87,
"battery_critical": false,
"unlatch_count": 34,
"lock_count": 34,
"total_actions": 68
}
}
nuki_battery_low — one-shot alert when battery drops below 20% (resets after recovery above 25%):