openapi: 3.1.0
info:
  title: Kiosk API
  version: 0.1.0
  description: >
    Standalone time capture kiosk API for tenant setup, API keys, device setup,
    Android kiosk management, candidate sync, face/PIN clock events, and
    external integrations.
servers:
  - url: https://kiosk.bosstotal.com
  - url: http://localhost:8088
security:
  - ApiKey: []
tags:
  - name: Auth
  - name: Tenants
  - name: API keys
  - name: Clients
  - name: Setup
  - name: Devices
  - name: Candidates
  - name: Clock events
  - name: Device API
  - name: Downloads
components:
  securitySchemes:
    ApiKey:
      type: http
      scheme: bearer
      description: Tenant API key beginning with `ksk_live_`.
    DashboardSession:
      type: http
      scheme: bearer
      description: Dashboard session token beginning with `kss_`.
    DeviceToken:
      type: http
      scheme: bearer
      description: Device token returned by setup claim or direct device issue.
  schemas:
    Tenant:
      type: object
      required: [id, slug, name]
      properties:
        id: { type: integer }
        slug: { type: string }
        name: { type: string }
        owner_email: { type: string }
    User:
      type: object
      required: [id, tenant_id, name, email, role]
      properties:
        id: { type: integer }
        tenant_id: { type: integer }
        name: { type: string }
        email: { type: string }
        role: { type: string }
    Client:
      type: object
      required: [id, tenant_id, name]
      properties:
        id: { type: integer }
        tenant_id: { type: integer }
        name: { type: string }
        external_ref: { type: string }
    Device:
      type: object
      required: [id, tenant_id, client_id]
      properties:
        id: { type: integer }
        tenant_id: { type: integer }
        client_id: { type: integer }
        label: { type: string }
        platform: { type: string }
        managed: { type: boolean }
        online: { type: boolean }
        last_seen_at: { type: string, format: date-time }
        last_network_status: { type: string }
        last_battery_level: { type: integer }
        last_update_status: { type: string }
    Candidate:
      type: object
      required: [id, tenant_id, client_id, external_id, display_name]
      properties:
        id: { type: integer }
        tenant_id: { type: integer }
        client_id: { type: integer }
        external_id: { type: string }
        display_name: { type: string }
        active: { type: boolean }
        metadata: { type: object }
    ClockEvent:
      type: object
      required: [id, tenant_id, event_type, source]
      properties:
        id: { type: integer }
        tenant_id: { type: integer }
        client_id: { type: integer }
        candidate_id: { type: integer }
        external_candidate_id: { type: string }
        event_type: { type: string, enum: [clock_in, clock_out] }
        source: { type: string, enum: [api, device, legacy] }
        occurred_at: { type: string, format: date-time }
        idempotency_key: { type: string }
    Error:
      type: string
paths:
  /health:
    get:
      tags: [Downloads]
      security: []
      summary: Service health.
      responses:
        '200':
          description: Service health.
  /docs:
    get:
      tags: [Downloads]
      security: []
      summary: Dashboard documentation page.
      responses:
        '302':
          description: Redirects to the dashboard docs view.
  /openapi/kiosk.v1.yaml:
    get:
      tags: [Downloads]
      security: []
      summary: Public OpenAPI contract.
      responses:
        '200':
          description: YAML OpenAPI document.
  /v1/auth/register:
    post:
      tags: [Auth]
      security: []
      summary: Create an account, tenant, owner user, dashboard session, and first root API key.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [tenant_slug, tenant_name, email, password]
              properties:
                tenant_slug: { type: string, example: acme }
                tenant_name: { type: string, example: Acme Labour }
                owner_name: { type: string, example: Alex Owner }
                email: { type: string, example: owner@example.com }
                password: { type: string, minLength: 8 }
      responses:
        '201':
          description: Account created. The root API key is returned once.
  /v1/auth/login:
    post:
      tags: [Auth]
      security: []
      summary: Sign in to the dashboard with email and password.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email: { type: string }
                password: { type: string }
      responses:
        '200':
          description: Dashboard session created.
  /v1/auth/me:
    get:
      tags: [Auth]
      security:
        - DashboardSession: []
        - ApiKey: []
      summary: Current dashboard user or API key tenant.
      responses:
        '200':
          description: Current auth context.
  /v1/auth/logout:
    post:
      tags: [Auth]
      security:
        - DashboardSession: []
      summary: Revoke the current dashboard session.
      responses:
        '200':
          description: Logged out.
  /v1/tenants:
    post:
      tags: [Tenants]
      security: []
      summary: Create tenant and first root API key.
      responses:
        '201':
          description: Tenant created.
  /v1/tenants/me:
    get:
      tags: [Tenants]
      summary: Current tenant.
      responses:
        '200':
          description: Tenant details.
  /v1/api-keys:
    get:
      tags: [API keys]
      summary: List redacted API keys.
      responses:
        '200':
          description: API key list.
    post:
      tags: [API keys]
      summary: Create scoped API key for an external app.
      responses:
        '201':
          description: API key created. Raw key is returned once.
  /v1/clients:
    get:
      tags: [Clients]
      summary: List clients/sites.
      responses:
        '200':
          description: Client list.
    post:
      tags: [Clients]
      summary: Create client/site.
      responses:
        '201':
          description: Client created.
  /v1/setup-tokens:
    post:
      tags: [Setup]
      summary: Create Android or PIN-web setup token and QR links.
      responses:
        '201':
          description: Setup token created.
  /setup/{token}:
    get:
      tags: [Setup]
      security: []
      summary: Browser setup page with APK links and setup token.
      parameters:
        - name: token
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Setup page.
  /setup/{token}.svg:
    get:
      tags: [Setup]
      security: []
      summary: QR SVG encoding the setup URL.
      parameters:
        - name: token
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Setup QR SVG.
  /android/latest.apk:
    get:
      tags: [Downloads]
      security: []
      summary: Latest face kiosk APK when published.
      responses:
        '200':
          description: Android package.
        '404':
          description: APK not published.
  /android/agent/latest.apk:
    get:
      tags: [Downloads]
      security: []
      summary: Latest device agent APK when published.
      responses:
        '200':
          description: Android package.
        '404':
          description: APK not published.
  /v1/devices:
    get:
      tags: [Devices]
      summary: List tenant devices.
      responses:
        '200':
          description: Device list.
    post:
      tags: [Devices]
      summary: Issue direct device token.
      responses:
        '201':
          description: Device token returned once.
  /v1/devices/{id}/commands:
    post:
      tags: [Devices]
      summary: Queue a remote device command.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      responses:
        '201':
          description: Command queued.
  /v1/candidates/upsert:
    post:
      tags: [Candidates]
      summary: Upsert candidate/person for a client.
      responses:
        '200':
          description: Candidate upserted.
  /v1/candidates:
    get:
      tags: [Candidates]
      summary: List candidates.
      responses:
        '200':
          description: Candidate list.
  /v1/clock-events:
    get:
      tags: [Clock events]
      summary: List clock events.
      responses:
        '200':
          description: Clock event list.
    post:
      tags: [Clock events]
      summary: Create clock event from an integration.
      responses:
        '201':
          description: Clock event accepted.
  /v1/device/claim:
    post:
      tags: [Device API]
      security: []
      summary: Claim a setup token from kiosk or agent.
      responses:
        '201':
          description: Device token returned.
  /v1/device/authorize:
    post:
      tags: [Device API]
      security:
        - DeviceToken: []
      summary: Authorize configured kiosk device.
      responses:
        '200':
          description: Device authorized.
  /v1/device/status:
    post:
      tags: [Device API]
      security:
        - DeviceToken: []
      summary: Report device status.
      responses:
        '200':
          description: Status accepted.
  /v1/device/pin/lookup:
    post:
      tags: [Device API]
      security:
        - DeviceToken: []
      summary: Resolve a candidate by PIN on the current device/client.
      responses:
        '200':
          description: Candidate and assignment returned.
        '404':
          description: PIN not found.
  /v1/device/face/enroll:
    post:
      tags: [Device API]
      security:
        - DeviceToken: []
      summary: Store a candidate face embedding captured by a kiosk.
      responses:
        '200':
          description: Face embedding stored.
  /v1/device/face/identify:
    post:
      tags: [Device API]
      security:
        - DeviceToken: []
      summary: Match face embeddings against enrolled candidates.
      responses:
        '200':
          description: Match result.
  /v1/device/clock-events:
    post:
      tags: [Device API]
      security:
        - DeviceToken: []
      summary: Create clock event from a device.
      responses:
        '201':
          description: Clock event accepted.
  /v1/device/commands:
    get:
      tags: [Device API]
      security:
        - DeviceToken: []
      summary: Poll pending commands.
      responses:
        '200':
          description: Pending commands.
  /v1/device/commands/{id}/ack:
    post:
      tags: [Device API]
      security:
        - DeviceToken: []
      summary: Acknowledge command.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      responses:
        '200':
          description: Command acknowledged.
  /v1/device/updates/manifest:
    get:
      tags: [Device API]
      security:
        - DeviceToken: []
      summary: Get Android update manifest.
      responses:
        '200':
          description: Update manifest.
