Worksonar Mobile API v1
REST-API für die geplante native Mitarbeiter-App. Klar getrennt von der Admin-API (/api/v1). Diese Seite ist auch als Markdown verfügbar; siehe auch die Mobile-Roadmap.
Inhalt
1. Einleitung
Die Mobile-API liefert die Teilnehmer-Sicht für native iOS/Android-Apps: Survey-Schemas, Antwort-Submission, Push-Token-Registrierung. Sie ist nicht die Admin-API (die heißt weiter /api/v1/...). Mandantenisolation ist über den JWT erzwungen.
| Umgebung | Base-URL |
|---|---|
| Diese Instanz | https://beta.worksonar.de/api/mobile/v1 |
| Hosted (Beispiel) | https://app.worksonar.de/api/mobile/v1 |
Versionierung über das URL-Präfix (/mobile/v1). Breaking Changes erscheinen ausschließlich in einer neuen Major-Version mit mindestens 6 Monaten Parallelbetrieb.
2. Authentifizierung
Alle geschützten Endpoints erwarten einen JWT im Authorization: Bearer <jwt>-Header. Der JWT wird ausschließlich auf dem Server signiert (HMAC-SHA256) und ist 7 Tage gültig.
3. Auth-Endpoints (kein JWT erforderlich)
POST /auth/by-token
Tauscht einen Long-lived Participant-Token (aus dem Einladungs-Link) gegen einen Mobile-JWT.
{ "token": "PARTICIPANT_TOKEN" }
Response 200: { jwt, expires_at, participant: {…}, tenant: {…} }
POST /auth/by-access-code
Anonyme Teilnahme via 6–8-stelligen Kampagnen-Access-Code. Der JWT ist auf diese Kampagne beschränkt (anon: true).
{ "access_code": "CON-GBU" }
POST /auth/magic-link/request
Schickt einen 6-stelligen Code + Deep-Link per E-Mail. Aus Datenschutzgründen wird immer 200 OK geantwortet — auch wenn die E-Mail unbekannt ist.
{ "email": "max@firma.de" }
POST /auth/magic-link/verify
Verifiziert den Code zusammen mit dem aus dem Deep-Link extrahierten link_token. Der Code allein reicht nicht aus (Brute-Force-Schutz).
{ "code": "123456", "link_token": "..." }
4. Geschützte Endpoints (JWT erforderlich)
GET /me
Profil + Liste aktiver Kampagnen mit Status (pending/invited/completed).
GET /campaigns/:cid
Liefert Survey-Schema, Submission-Regeln und Anonymitätsschwelle.
POST /campaigns/:cid/submit
Speichert Antworten und markiert den Teilnehmer als completed.
{
"answers": { "vollst_1": 4, "interest_sachbezugskarte": 5 },
"department": "Vertrieb", // nur bei anonymem JWT
"location": "Berlin" // nur bei anonymem JWT
}
POST /devices/register
Push-Token (FCM/APNs) für diesen Teilnehmer registrieren. Upsert auf push_token + participant_id.
{
"push_token": "fcm-token-string...",
"platform": "ios", // ios | android | web
"device_name": "iPhone von Max",
"app_version": "1.0.3",
"locale": "de"
}
GET /devices · DELETE /devices/:id
Eigene registrierte Geräte auflisten und einzeln widerrufen (Logout-Funktion in der App).
5. Schema-Formate je Modul
Der Endpoint GET /campaigns/:cid liefert je nach Modul ein passendes Schema:
| Modul | Felder |
|---|---|
gbu_psych | sections[].questions[] mit Likert-Skala 1–4 |
benefits | profile_questions, existing, categories, catalog, category_intros |
bgm | wie Benefits + who5: [{ key, text }] |
| Custom-Modul | { type: 'custom', module: {...} } mit eigenem Schema-Format |
6. Fehler-Format
{ "error": { "code": "validation", "message": "...", "details": {...} } }
| HTTP | Codes |
|---|---|
| 400 | validation |
| 401 | unauthenticated · invalid_token |
| 403 | plan_restriction · tenant_inactive |
| 404 | not_found |
| 422 | campaign_inactive |
| 429 | rate_limited |
7. Rate-Limits
Auth-Endpoints sind auf 30 Versuche / IP / Minute begrenzt. Geschützte Endpoints haben kein hartes Limit, werden aber auf dem Server überwacht.
8. JWT-Format
HS256-signiertes JSON, ohne JWT-Standard-Header (Format: <b64url(payload)>.<b64url(hmac-sha256)>).
{
"sub": "participant-id",
"tid": "tenant-id",
"anon": false,
"cid": "campaign-id", // nur bei anonymer Session
"iat": 1718208000,
"exp": 1718812800 // +7 Tage
}
Constant-Time-Vergleich gegen Timing-Angriffe. Geheimnis wird deterministisch aus SESSION_SECRET abgeleitet.
9. Versionierung
Additive Erweiterungen (neue Felder, neue Endpoints) gelten als non-breaking; die App muss unbekannte Felder ignorieren. Breaking Changes nur in neuer Major-Version (/mobile/v2) mit Parallelbetrieb von mindestens 6 Monaten.
10. End-to-End-Flow (Beispiel)
// 1) Anmelden mit Deep-Link aus Einladungs-Mail
const auth = await fetch(API + '/auth/by-token', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: deepLinkParams.token }),
}).then(r => r.json());
// 2) Aktive Befragungen laden
const me = await fetch(API + '/me', {
headers: { Authorization: 'Bearer ' + auth.jwt }
}).then(r => r.json());
// 3) Schema laden & Antworten submitten
const survey = await fetch(API + `/campaigns/${me.campaigns[0].id}`, {
headers: { Authorization: 'Bearer ' + auth.jwt }
}).then(r => r.json());
await fetch(API + `/campaigns/${campaignId}/submit`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + auth.jwt },
body: JSON.stringify({ answers: collectedAnswers }),
});