| .gemini | ||
| fast-test.json | ||
| GEMINI.md | ||
| README.md | ||
| test_workflow.json | ||
| tags | created | status | |||||||
|---|---|---|---|---|---|---|---|---|---|
|
2026-10-01 | in-progress |
Virtuagym – Automatisiertes Mitglieder-Onboarding
1. Projektbeschreibung
Für ein Fitnessstudio soll ein automatisierter Onboarding-Prozess für neue Mitglieder etabliert werden. Wenn in Virtuagym ein neuer Vertrag abgeschlossen wird, soll automatisch ein EVVA AirKey-Zugangsschlüssel erzeugt und dem neuen Mitglied zugestellt werden.
Beteiligte Systeme
| System | Rolle |
|---|---|
| Virtuagym | Mitgliederverwaltung / Vertragssoftware |
| EVVA AirKey | Zutrittskontrolle (digitale Smartphone-Keys) |
| n8n | Middleware / Automatisierung (self-hosted auf OMV6) |
Geplanter Ablauf
flowchart LR
A["n8n Polling\n(alle 10 Min)"] -->|sync_from| B["Virtuagym API"]
B -->|Neues Mitglied| C["n8n Workflow"]
C -->|send-a-key| D["AirKey API"]
D --> E["Mitglied erhält\ndigitalen Schlüssel"]
2. Protokoll
2026-10-01 – Projektstart & n8n Setup
Schritt 1: n8n installieren (Docker auf OMV6)
Als Testumgebung dient ein kleiner Server mit OpenMediaVault 6 (OMV6). n8n wird als Docker-Container via Docker Compose betrieben.
Voraussetzungen auf dem OMV6-Server:
- Docker & Docker Compose installiert (OMV6 → Plugins → openmediavault-compose)
- Netzwerkzugang auf Port
5678
Compose-Datei:
services:
n8n:
image: n8nio/n8n:latest
container_name: n8n
restart: unless-stopped
ports:
- "5678:5678"
environment:
- N8N_HOST=0.0.0.0
- N8N_PORT=5678
- N8N_PROTOCOL=http
- GENERIC_TIMEZONE=Europe/Berlin
- TZ=Europe/Berlin
- N8N_ENCRYPTION_KEY=change-me-to-a-random-string
# Webhook-URL anpassen wenn externer Zugriff nötig:
# - WEBHOOK_URL=https://n8n.example.com/
volumes:
- n8n_data:/home/node/.n8n
volumes:
n8n_data:
Deployment:
docker compose up -d
docker compose ps
docker compose logs -f n8n
n8n ist danach erreichbar unter: http://<server-ip>:5678
Important
Beim ersten Aufruf wird ein Owner-Account erstellt.
N8N_ENCRYPTION_KEYunbedingt durch einen sicheren Zufallswert ersetzen – dieser Schlüssel verschlüsselt alle gespeicherten Credentials.
Status: ✅ n8n läuft und ist von außen erreichbar (Stand 2026-10-04)
2026-10-01 – API-Recherche
2026-10-04 – Offizielle Virtuagym API-Doku eingelesen
Schritt 2: Virtuagym API
Offizielle API-Doku: ~/repo/api-documentation-master (privates GitHub-Repo, als ZIP heruntergeladen)
Zugang & Authentifizierung
- Base URL:
https://api.virtuagym.com/api/v1/club/{club_id} - HTTPS ist Pflicht für alle Requests
Credentials (unter System Settings → Business Info → Advanced):
| Credential | Parameter-Name | Beschreibung |
|---|---|---|
| Club-ID | club_id |
Numerische Club-ID (in der URL) |
| API-Key | api_key |
API-Schlüssel |
| Club Key | club_secret |
⚠️ Heißt in der API club_secret, nicht club_key! |
Auth-Methode: Query-Parameter auf jedem Request:
GET /api/v1/club/{club_id}/member?api_key={api_key}&club_secret={club_secret}
Webhooks & Polling
Warning
Virtuagym bietet KEINE Webhooks an. Bestätigt durch offizielle Doku.
Polling-Lösung: Der sync_from-Parameter liefert nur Einträge zurück, die nach dem angegebenen Timestamp geändert wurden.
Important
Timestamps sind in Millisekunden, nicht Sekunden! (z.B.
timestamp_edit: 1429522480252)
n8n Schedule Trigger (Cron/Intervall) pollt regelmäßig und speichert den letzten Timestamp via $getWorkflowStaticData().
Rate-Limit: 500 Requests/Stunde (lt. offizieller Doku)
| Intervall | Calls/Tag |
|---|---|
| 5 Min | 288 |
| 10 Min | 144 ✅ |
| 15 Min | 96 |
Endpoint: Mitglieder abrufen
GET /api/v1/club/{club_id}/member
| Parameter | Typ | Pflicht | Beschreibung |
|---|---|---|---|
api_key |
string | ✅ | API-Schlüssel |
club_secret |
string | ✅ | Club Key |
sync_from |
int | - | Timestamp in Millisekunden – nur Änderungen danach |
from_id |
int | - | Pagination via member_id (besser bei gleichen Timestamps) |
club_member_id |
string | - | Filter nach eigener Member-ID |
external_id |
string | - | Filter nach externer ID |
email |
string | - | Filter nach E-Mail |
rfid_tag |
string | - | Filter nach RFID-Tag |
with |
string | - | memberships oder active_memberships – Verträge direkt mitladen! |
Tip
with=active_membershipsspart einen extra API-Call! Mitglied + aktive Verträge kommen in einer einzigen Response.
Antwort-Felder:
| Feld | Typ | Beschreibung |
|---|---|---|
member_id |
int | Eindeutige ID |
club_id |
int | Club-Zugehörigkeit |
club_member_id |
string | Eigene Member-ID des Studios |
external_id |
string | Externe System-ID |
firstname |
string | Vorname → AirKey Person |
lastname |
string | Nachname → AirKey Person |
email |
string | E-Mail → AirKey /send-a-key/mail |
phone |
string | Telefon |
mobile |
string | Mobil → AirKey /send-a-key/sms |
active |
boolean | Aktiv/Inaktiv |
member_since |
int | Mitglied seit (Timestamp in ms) |
timestamp_edit |
int | Letzte Änderung (Timestamp in ms) |
rfid_tag |
string | RFID-Tag |
Beispiel-Response (mit with=memberships):
{
"statuscode": 200,
"result": [
{
"member_id": 78166,
"firstname": "ABC",
"lastname": "DEF",
"email": "abcdef@example.com",
"active": true,
"member_since": 1453417200000,
"timestamp_edit": 1453471427081,
"memberships": [
{
"instance_id": 2724,
"member_id": 78166,
"membership_id": 593,
"active": 1,
"cancelled": 0,
"paused": 0,
"stopped": 0,
"start_date": "2016-06-09",
"contract_start_date": "2016-07-01",
"contract_end_date": "2016-07-31",
"membership_name": "Premium 12M"
}
]
}
]
}
Endpoint: Membership Instances (Verträge)
GET /api/v1/club/{club_id}/membership/instance
| Parameter | Typ | Beschreibung |
|---|---|---|
sync_from |
int | Timestamp in ms – nur Änderungen danach |
member_id |
int | Filter nach Mitglied |
from_id |
int | Pagination via instance_id |
Antwort-Felder:
| Feld | Typ | Beschreibung |
|---|---|---|
instance_id |
int | Vertrags-ID |
member_id |
int | Zugehöriges Mitglied |
membership_id |
int | Referenz auf Membership Definition |
membership_name |
string | Vertragsname |
active |
boolean | Vertrag aktiv? |
cancelled |
boolean | Manuell gekündigt? |
paused |
boolean | Pausiert? |
stopped |
boolean | Sofort beendet? |
completed |
boolean | Regulär ausgelaufen? |
contract_autorenewed |
boolean | Bereits verlängert? |
start_date |
string | Startdatum (YYYY-MM-DD) |
contract_start_date |
string | Vertragsbeginn (YYYY-MM-DD) |
contract_end_date |
string | Vertragsende (YYYY-MM-DD) |
Endpoint: Membership Definitions (Produkt-Katalog)
GET /api/v1/club/{club_id}/membership/definition
Liefert den Produkt-Katalog mit Preisen, Laufzeiten und Zutrittszeiten:
| Feld | Beschreibung |
|---|---|
membership_id |
Produkt-ID |
membership_name |
Name (z.B. "Premium 12M") |
membership_duration |
Laufzeit (z.B. 12) |
membership_duration_type |
weeks / months |
membership_auto_renew |
Automatische Verlängerung? |
membership_price |
Preis |
membership_price_term |
monthly / weekly / four_weekly / total |
access_times |
Array mit Zutrittszeiten pro Wochentag! |
Tip
Die
access_timesaus der Membership Definition könnten direkt als AirKey-Zeitfenster übernommen werden!
Beispiel access_times:
"access_times": [
{ "day": "Monday", "start_time": "06:00:00", "end_time": "22:00:00" },
{ "day": "Tuesday", "start_time": "06:00:00", "end_time": "22:00:00" }
]
HTTP Status Codes
| Code | Bedeutung |
|---|---|
200 |
OK |
201 |
Created (z.B. neues Mitglied) |
401 |
Ungültige/fehlende Credentials |
404 |
Resource nicht gefunden |
420 |
Business-Logic-Fehler (z.B. Member nicht gefunden) |
423 |
Concurrent Update (Member wird gerade aktualisiert) |
Kosten & Voraussetzungen
- €10/Monat pro Standort als API-Add-on
- Paket „Manage" oder „Manage & Grow" erforderlich
- Module Clients & Staff + Membership & Billing müssen aktiv sein
Schritt 3: EVVA API (AirKey bestätigt ✅)
AirKey vs. Xesar
| Kriterium | AirKey | Xesar |
|---|---|---|
| Architektur | Cloud-native SaaS | On-Premise Server |
| API-Typ | REST (JSON/HTTPS) | MQTT + lokale REST |
| Authentifizierung | API Key (X-API-Key Header) + IP-Whitelist |
Mutual TLS (mTLS) |
| Remote Key-Versand | ✅ SMS & E-Mail Endpoints | Eingeschränkt (SMS via XMS) |
| Doku | Öffentliches Swagger UI | Nur über Partner-Portal |
Authentifizierung
- HTTP Header:
X-API-Key: <key> - Bis zu 10 API-Keys im AirKey-Portal unter
Settings → Cloud Interface - Optional: IP-Whitelisting
- Alle Kommunikation über HTTPS/TLS 1.2+
Der "Send a Key" Endpoint (Kernstück für unser Szenario!)
AirKey bietet All-in-One-Endpoints, die in einem einzigen Call:
- Person anlegen
- Smartphone-Medium erstellen
- Zugangsrechte zuweisen
- Onboarding-Link per SMS oder E-Mail versenden
| Endpoint | Beschreibung |
|---|---|
POST /cloud/v1/send-a-key/sms |
Schlüssel per SMS versenden |
POST /cloud/v1/send-a-key/mail |
Schlüssel per E-Mail versenden |
Ablauf beim Mitglied:
- SMS/E-Mail mit Onboarding-Link erhalten
- EVVA AirKey App installieren (iOS/Android)
- Registrierungscode aktivieren
- Smartphone generiert kryptographisches Schlüsselpaar
- Zukünftige Berechtigungsänderungen kommen per Push-Notification
Weitere relevante Endpoints
| Endpoint | Beschreibung |
|---|---|
GET/POST /v1/persons |
Personen verwalten |
GET /v1/media/phones |
Smartphone-Medien auflisten |
POST /v1/authorizations/simple |
Zeitgebundene Zugangsrechte |
POST /v1/authorizations/advanced |
Komplexe Zeitpläne (Wochentage etc.) |
DELETE /v1/authorizations |
Zugang sofort entziehen |
GET /v1/locks |
Schließzylinder auflisten |
GET /v1/areas |
Logische Bereiche (Türgruppen) |
GET /v1/lock-protocol-limit |
Audit-Log (wer hat wann wo geöffnet) |
Dokumentation & Test-Umgebung
- Swagger UI: https://integration.api.airkey.evva.com/docs/
- OpenAPI JSON: https://integration.api.airkey.evva.com/docs/swagger.json
- Demo-Umgebung: https://apidemo.airkey.evva.com/
- Sandbox Reset:
POST /v1/public-mgmt/reset-test-data - Production Base URL:
https://api.airkey.evva.com/cloud/v1 - GitHub: https://github.com/evva-sfw
Kosten
- Aktivierung Cloud Interface: einmalig 350 KeyCredits
- Pro Schlüsseländerung: 1 KeyCredit (lesen/löschen ist kostenlos)
- KeyCredit Unlimited: Flatrate für 12 oder 36 Monate (empfohlen bei hohem Durchsatz)
- Sandbox/Test: komplett kostenlos
3. Integrationsarchitektur
Geplanter n8n-Workflow
flowchart TD
A["⏰ n8n Schedule Trigger\n(alle 10 Min)"] --> B["HTTP Request: GET\nVirtuagym /member\n?sync_from=...&with=active_memberships"]
B --> C{"Neues Mitglied\nmit aktivem Vertrag?"}
C -->|Nein| D["Ende"]
C -->|Ja| E["Mitgliederdaten\nextrahieren\n(Name, Mobile/Email,\nVertragslaufzeit)"]
E --> F["HTTP Request: POST\nAirKey /send-a-key/sms\noder /send-a-key/mail"]
F --> G["Mitglied erhält\ndigitalen Schlüssel"]
G --> H["Static Data:\nsync_from aktualisieren"]
Ablauf im Detail
- Trigger: n8n Schedule Trigger alle 10 Minuten
- Polling:
GET /member?sync_from={lastSync}&with=active_memberships– ein einziger Call liefert Mitglied + Vertragsdaten - Filter: Nur Mitglieder bei denen
timestamp_edit>member_since(annähernd gleich = neu erstellt) UND mindestens eineactiveMembership - Mapping: Virtuagym-Daten → AirKey-Format:
firstname+lastname→ AirKey Personmobileoderemail→ Versandkanalcontract_end_date→ AirKeyvalidToaccess_times(aus Membership Definition) → AirKey Zeitfenster
- Key-Versand:
POST /cloud/v1/send-a-key/smsoder/mail - State Update:
sync_fromauf aktuellen Timestamp setzen via$getWorkflowStaticData('node')
4. Offene Aufgaben
Sofort
- n8n auf OMV6 deployen ✅
- n8n von außen erreichbar ✅
- Virtuagym API-Zugang beantragt und erhalten ✅
- Offizielle API-Doku eingelesen ✅
- EVVA AirKey Cloud Interface aktivieren (350 KeyCredits)
- AirKey Sandbox testen (https://integration.api.airkey.evva.com/docs/)
Klärungsbedarf
Welches EVVA-System ist verbaut?→ AirKey bestätigt ✅- Schlüsselversand per SMS oder E-Mail? (oder beides?)
- Welche Türen/Bereiche sollen Neumitglieder erhalten?
- Gültigkeitszeitraum der Schlüssel = Vertragslaufzeit?
- Sollen
access_timesaus der Membership Definition als AirKey-Zeitfenster übernommen werden?
Später
- n8n-Workflow bauen und testen
- Fehlerbehandlung (z.B. ungültige Telefonnummer, AirKey-Fehler)
- Vertragskündigung → automatischer Schlüsselentzug?
- End-to-End-Test mit echtem Mitglied