Skip to content

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:

{
  "access_token": "eyJ...",
  "token_type": "bearer",
  "expires_in": 900
}

Use the token in subsequent requests:

Authorization: Bearer eyJ...

Tokens expire after 15 minutes. Refresh with:

POST /api/v1/auth/refresh
Authorization: Bearer <current-token>

Device API

Uses a pre-shared device token, provisioned during ESP32 setup:

Authorization: Bearer <device-token>

Admin API Endpoints

Members

List Members

GET /api/v1/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

GET /api/v1/members/{id}

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.

DELETE /api/v1/members/{id}

NFC Keys

List Keys for a Member

GET /api/v1/members/{member_id}/keys

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

DELETE /api/v1/nfc-keys/{id}

Sets revoked_at timestamp. The key is excluded from the next allow-list generation.

Access Rules

List Rules

GET /api/v1/access-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:

{
  "sponsor_member_id": "uuid",
  "max_duration_days": 7
}

role_minimum — minimum role required:

{
  "minimum_role": "keyholder"
}

global_override — emergency controls:

{
  "action": "lockdown"
}

or:

{
  "action": "open_all"
}

Update Rule

PUT /api/v1/access-rules/{id}
Content-Type: application/json

{
  "name": "Updated name",
  "is_active": false
}

Delete Rule

DELETE /api/v1/access-rules/{id}

Audit Log

Query Audit Log

GET /api/v1/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

GET /api/v1/health

Response:

{
  "status": "ok",
  "service": "openlatch"
}

Device API Endpoints

Fetch Allow-List

GET /api/v1/device/allowlist?current_version=42
Authorization: Bearer <device-token>

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):

{"approved": true}

or:

{"approved": false, "reason": "Organizer denied"}

or:

{"approved": false, "reason": "timeout"}

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
}