openapi: 3.1.0 info: title: Getraenkeautomat Monitor API version: 1.0.0 summary: HTTP API for ingesting sensor readings and retrieving vending machine status. description: | Handwritten OpenAPI specification for the Getraenkeautomat Monitor application. The API currently exposes two endpoints: - `POST /api/v1/readings.php` for ingesting one sensor measurement - `GET /api/v1/status.php` for retrieving the aggregated application state The readings endpoint uses Bearer token authentication. The status endpoint is intentionally public so the dashboard can poll it without a login. servers: - url: ./ description: Same-origin deployment tags: - name: Readings description: Receive one sensor reading and update the persisted slot state. - name: Status description: Retrieve the current aggregated machine, slot, and alert status. paths: /api/v1/readings.php: post: tags: - Readings summary: Submit one reading description: | Accepts exactly one sensor measurement for one configured machine slot. Processing behavior: - Requires a Bearer token in the `Authorization` header - Accepts JSON request bodies - Updates `data/state.json` on success - Triggers alerts only when the slot state changes from `ok` to `critical` or from `critical` to `ok` operationId: submitReading security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ReadingRequest' examples: lobbySlot: summary: Reading for the A1 slot in the lobby machine value: machine_id: automat-lobby sensor_id: fach-a1 distance_mm: 184 measured_at: '2026-04-15T19:20:00Z' responses: '200': description: Reading processed successfully. content: application/json: schema: $ref: '#/components/schemas/ReadingSuccessResponse' examples: ok: value: ok: true machine_id: automat-lobby sensor_id: fach-a1 slot_label: A1 units_estimated: 4 fill_percent: 63 state: ok '400': description: Invalid JSON body or malformed request payload. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: invalidJson: value: ok: false error: Ungueltiger JSON-Body. '401': description: Missing or invalid Bearer token. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: unauthorized: value: ok: false error: Nicht autorisiert. '404': description: The referenced machine or sensor is not configured. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: unknownMachineOrSensor: value: ok: false error: Unbekannter Automat oder Sensor. '405': description: Only POST is supported for this endpoint. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: wrongMethod: value: ok: false error: Nur POST ist erlaubt. '422': description: Semantic validation failed. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: missingIdentifiers: value: ok: false error: machine_id und sensor_id sind erforderlich. nonNumericDistance: value: ok: false error: distance_mm muss numerisch sein. invalidTimestamp: value: ok: false error: measured_at ist kein gueltiger ISO-Zeitstempel. '500': description: Unexpected internal server error. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: internalError: value: ok: false error: Interner Fehler. options: tags: - Readings summary: CORS preflight for readings description: | Preflight handler for browser-based clients. The endpoint responds with `204 No Content` and emits `Access-Control-Allow-Methods` and `Access-Control-Allow-Headers`. operationId: readingsPreflight responses: '204': description: Preflight accepted without a response body. headers: Access-Control-Allow-Methods: description: Allowed methods for this endpoint. schema: type: string example: POST, OPTIONS Access-Control-Allow-Headers: description: Allowed request headers for this endpoint. schema: type: string example: Authorization, Content-Type /api/v1/status.php: get: tags: - Status summary: Retrieve the current application status description: | Returns the aggregated state for the dashboard and admin panel. The response contains: - app metadata - machine and slot status - a summary section - the most recent alert log entries operationId: getStatus responses: '200': description: Aggregated status generated successfully. content: application/json: schema: $ref: '#/components/schemas/StatusResponse' examples: dashboard: value: ok: true generated_at: '2026-04-15T20:10:00+00:00' app: name: Getraenkeautomat Monitor dashboard_refresh_seconds: 15 summary: machine_count: 2 slot_count: 3 critical_count: 1 machines: - id: automat-lobby name: Lobby Automat location: Erdgeschoss slots: - machine_id: automat-lobby machine_name: Lobby Automat sensor_id: fach-a1 slot_label: A1 product_name: Cola 0,5l fill_percent: 63 units_estimated: 4 max_units: 7 distance_mm: 184 state: ok measured_at: '2026-04-15T19:20:00+00:00' updated_at: '2026-04-15T19:20:02+00:00' alert_below_units: 2 webhook_ids: - lager-webhook email_ids: - lager-team alerts: - id: alert_680004979d8512.07480974 created_at: '2026-04-15T19:20:02+00:00' payload: event: critical machine_id: automat-lobby machine_name: Lobby Automat sensor_id: fach-a1 slot_label: A1 product_name: Cola 0,5l distance_mm: 320 units_estimated: 1 max_units: 7 fill_percent: 14 state: critical previous_state: ok measured_at: '2026-04-15T19:19:59+00:00' deliveries: webhooks: - id: lager-webhook success: false message: Webhook nicht gefunden oder deaktiviert. emails: - id: lager-team success: true message: Email versendet. '405': description: Only GET is supported for this endpoint. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: wrongMethod: value: ok: false error: Nur GET ist erlaubt. components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: opaque token description: Bearer token stored in config.json under api.bearer_token. schemas: ReadingRequest: type: object additionalProperties: false required: - machine_id - sensor_id - distance_mm properties: machine_id: type: string description: Configured machine identifier from config.json. minLength: 1 example: automat-lobby sensor_id: type: string description: Slot or sensor identifier within the machine. minLength: 1 example: fach-a1 distance_mm: type: number description: Measured distance in millimeters. example: 184 measured_at: type: - string - 'null' description: | Optional measurement timestamp. When omitted or empty, the server uses the current time. Values are parsed with PHP `strtotime()` and returned as an ISO-8601 timestamp. format: date-time example: '2026-04-15T19:20:00Z' ReadingSuccessResponse: type: object additionalProperties: false required: - ok - machine_id - sensor_id - slot_label - units_estimated - fill_percent - state properties: ok: type: boolean const: true machine_id: type: string example: automat-lobby sensor_id: type: string example: fach-a1 slot_label: type: string example: A1 units_estimated: type: integer example: 4 fill_percent: type: integer example: 63 state: $ref: '#/components/schemas/SlotState' ErrorResponse: type: object additionalProperties: false required: - ok - error properties: ok: type: boolean const: false error: type: string example: Nicht autorisiert. StatusResponse: type: object additionalProperties: false required: - ok - generated_at - app - summary - machines - alerts properties: ok: type: boolean const: true generated_at: type: string format: date-time description: Timestamp when the response was generated. app: $ref: '#/components/schemas/AppStatus' summary: $ref: '#/components/schemas/StatusSummary' machines: type: array items: $ref: '#/components/schemas/MachineStatus' alerts: type: array items: $ref: '#/components/schemas/AlertEvent' AppStatus: type: object additionalProperties: false required: - name - dashboard_refresh_seconds properties: name: type: string example: Getraenkeautomat Monitor dashboard_refresh_seconds: type: integer minimum: 1 example: 15 StatusSummary: type: object additionalProperties: false required: - machine_count - slot_count - critical_count properties: machine_count: type: integer minimum: 0 example: 2 slot_count: type: integer minimum: 0 example: 3 critical_count: type: integer minimum: 0 example: 1 MachineStatus: type: object additionalProperties: false required: - id - name - location - slots properties: id: type: string example: automat-lobby name: type: string example: Lobby Automat location: type: string example: Erdgeschoss slots: type: array items: $ref: '#/components/schemas/SlotStatus' SlotStatus: type: object additionalProperties: false required: - machine_id - machine_name - sensor_id - slot_label - product_name - fill_percent - units_estimated - max_units - distance_mm - state - measured_at - updated_at - alert_below_units - webhook_ids - email_ids properties: machine_id: type: string example: automat-lobby machine_name: type: string example: Lobby Automat sensor_id: type: string example: fach-a1 slot_label: type: string example: A1 product_name: type: string example: Cola 0,5l fill_percent: type: - integer - 'null' minimum: 0 maximum: 100 description: Null until a first reading has been received. example: 63 units_estimated: type: - integer - 'null' minimum: 0 description: Null until a first reading has been received. example: 4 max_units: type: integer minimum: 0 example: 7 distance_mm: type: - number - 'null' description: Last measured distance in millimeters. example: 184 state: $ref: '#/components/schemas/SlotState' measured_at: type: - string - 'null' format: date-time example: '2026-04-15T19:20:00+00:00' updated_at: type: - string - 'null' format: date-time example: '2026-04-15T19:20:02+00:00' alert_below_units: type: integer minimum: 0 example: 2 webhook_ids: type: array items: type: string example: - lager-webhook email_ids: type: array items: type: string example: - lager-team SlotState: type: string enum: - ok - critical - unknown example: ok AlertEvent: type: object additionalProperties: false required: - id - created_at - payload - deliveries properties: id: type: string description: Unique alert log entry ID generated with PHP uniqid(). example: alert_680004979d8512.07480974 created_at: type: string format: date-time example: '2026-04-15T19:20:02+00:00' payload: $ref: '#/components/schemas/AlertPayload' deliveries: $ref: '#/components/schemas/AlertDeliveries' AlertPayload: type: object additionalProperties: false required: - event - machine_id - machine_name - sensor_id - slot_label - product_name - distance_mm - units_estimated - max_units - fill_percent - state - previous_state - measured_at properties: event: type: string enum: - critical - recovered example: critical machine_id: type: string example: automat-lobby machine_name: type: string example: Lobby Automat sensor_id: type: string example: fach-a1 slot_label: type: string example: A1 product_name: type: string example: Cola 0,5l distance_mm: type: - number - 'null' example: 320 units_estimated: type: - integer - 'null' example: 1 max_units: type: - integer - 'null' example: 7 fill_percent: type: - integer - 'null' example: 14 state: $ref: '#/components/schemas/SlotState' previous_state: type: - string - 'null' description: Previous slot state before the transition. example: ok measured_at: type: - string - 'null' format: date-time example: '2026-04-15T19:19:59+00:00' AlertDeliveries: type: object additionalProperties: false required: - webhooks - emails properties: webhooks: type: array items: $ref: '#/components/schemas/DeliveryResult' emails: type: array items: $ref: '#/components/schemas/DeliveryResult' DeliveryResult: type: object additionalProperties: false required: - id - success - message properties: id: type: string example: lager-team success: type: boolean example: true message: type: string example: Email versendet.