|
|
@@ -0,0 +1,654 @@
|
|
|
+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.
|