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.
Health¶
Response:
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.
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: