Vigotime Planner API
Integriere die Dienstplanung headless: Du schickst einen self-contained Payload (Mitarbeiter, Schichten, Regeln, Abwesenheiten) — und bekommst einen fertigen Dienstplan zurück. Kein Wissen über die interne Datenhaltung nötig, kein Zwang zu unserer Oberfläche.
Überblick
Drei Endpoints, asynchron: absenden → Status pollen → Ergebnis holen.
Self-contained
Alle Entitäten reisen im Request mit — keine Vor-Synchronisation, kein Datenbankzugriff bei uns.
Asynchron
Der Solver läuft im Hintergrund; du pollst den Job-Status und holst das Ergebnis.
Pro Paket limitiert
Problemgröße, Solver-Zeit, Rate-Limit und Parallelität sind je Tarif konfiguriert.
Basis-URLs
| Umgebung | Basis-URL |
|---|---|
| Production | https://api.vigotime.com |
| Staging | https://staging-planner.vigotime.com |
| Lokal (dev) | http://localhost:8000 |
Authentifizierung
Jeder Aufruf trägt dein API-Token im X-API-Key-Header. Das Token ist
an deine Organisation gebunden — der Mandant kommt aus dem Token, nie aus einem Header.
Fünf Tokens einer Organisation teilen sich dieselben Limits.
X-API-Key: vt_live_<dein-token> # Produktion — kostet Token
X-API-Key: vt_test_<dein-token> # Sandbox — kostenlos, isolierte Test-Organisation
POST /api/v1/tokens).
Der Klartext wird genau einmal angezeigt — leg ihn in einem Secret-Manager ab, niemals im
Code. Widerruf ist sofort wirksam.Präfix vt_live_ vs. vt_test_: Sandbox-Tokens laufen gegen eine Test-Organisation,
werden nie abgerechnet und sind an der ersten Zeichenfolge erkennbar (auch für Secret-Scanner).
Datenschutz & Datenminimierung
Der Solver rechnet mit IDs, nicht mit Personen. Er braucht keine Namen,
keine Geburtsdaten, keine Klarnamen — nur eine id je Mitarbeiter und die fachlichen
Attribute (Qualifikationen, Vertragsstunden, Verfügbarkeit).
employee_id), du mappst sie lokal auf deine Mitarbeiter.Konkret: ein employees-Eintrag braucht id, qualificationIds,
contractHoursPerWeek — kein firstName/lastName.
Solve-Jobs werden zeitlich begrenzt aufbewahrt (Ergebnis-Retention) und sind an deine Organisation
gebunden; ohne Namen ist selbst dieser Bestand frei von Klardaten.
KI-Assistenten (MCP)
Der Planner ist ein MCP-Server: Claude, ChatGPT und Gemini können den
Solver direkt bedienen — „Plane den September für diese 6 Leute" wird zum Tool-Call. Endpoint:
https://api.vigotime.com/mcp (Staging: https://staging-planner.vigotime.com/mcp),
Auth wie bei der REST-API über X-API-Key. Es gelten dieselben Limits, Kosten und
Mandanten-Grenzen — der MCP-Zugang ist kein zweiter Weg an der Abrechnung vorbei.
Werkzeuge & eingebaute Prompts
| Tool | Zweck |
|---|---|
example_payload | Sofort lauffähiger Beispiel-Job (simple oder wishes mit Social Points) |
solve_submit | Solve-Job einreichen (self-contained Payload) |
solve_status | Status/Fortschritt pollen |
solve_result | Fertigen Plan abholen (assignments, unbesetzte Schichten mit Begründung) |
account_balance | Token-Guthaben der Organisation |
Dazu drei mitgelieferte Prompts: plan_woche, plan_mit_wuenschen
(Social-Points-Economy) und datenschutz_check (ersetzt Klarnamen durch IDs, bevor
etwas das Haus verlässt).
Claude einbinden
Claude Code (Terminal) — ein Befehl:
claude mcp add --transport http vigotime https://api.vigotime.com/mcp \
--header "X-API-Key: vt_test_<dein-sandbox-token>"
Claude Desktop (oder jeder Client ohne Header-Support) über die mcp-remote-Brücke —
in claude_desktop_config.json:
{
"mcpServers": {
"vigotime": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://api.vigotime.com/mcp",
"--header", "X-API-Key:${VIGOTIME_API_KEY}"],
"env": { "VIGOTIME_API_KEY": "vt_test_<dein-sandbox-token>" }
}
}
}
ChatGPT einbinden
Einstellungen → Connectors (Developer Mode aktivieren) → „Add server" →
URL https://api.vigotime.com/mcp. Trägt dein ChatGPT-Plan Custom-Header, setze
X-API-Key; andernfalls nutze übergangsweise die mcp-remote-Brücke
(Konfiguration wie bei Claude Desktop).
Gemini einbinden
Gemini CLI — in ~/.gemini/settings.json:
{
"mcpServers": {
"vigotime": {
"httpUrl": "https://api.vigotime.com/mcp",
"headers": { "X-API-Key": "vt_test_<dein-sandbox-token>" }
}
}
}
Zum Losspielen: drei Prompts
# 1 — Erster Plan in 2 Minuten
Hole dir mit example_payload ein Beispiel, löse es mit solve_submit,
polle solve_status bis completed und erkläre mir das Ergebnis:
Wer arbeitet wann, was blieb unbesetzt und warum?
# 2 — Eigener Mini-Plan
Plane den nächsten Monat für 5 Mitarbeiter (IDs m1–m5, je 38,5 h/Woche)
mit Früh- (06–14) und Spätdienst (14–22), mindestens 1 Person je Schicht.
m3 wünscht sich die ersten beiden Samstage frei (Priorität hoch).
# 3 — Social Points ausprobieren
Hole example_payload(kind='wishes') und erkläre mir, wie social_points
und wish_costs das Wunsch-Honorieren steuern. Ändere das Guthaben von
mitarbeiter-01 auf 20 und zeige den Unterschied im Ergebnis.
datenschutz_check-Prompt hilft dabei.Quickstart
Job absenden, Status pollen, Ergebnis holen — mit curl und jq.
BASE=https://staging-planner.vigotime.com # Produktion: https://api.vigotime.com
KEY=vt_test_<dein-sandbox-token>
# 1) Job absenden
JOB=$(curl -s -X POST "$BASE/api/v1/solve" \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{
"start_date": "2026-07-01",
"end_date": "2026-07-07",
"department": { "id": "d1", "name": "Pflege Station 1" },
"employees": [
{ "id": "e1", "primaryDepartmentId": "d1", "contractHoursPerWeek": 40, "qualificationIds": ["q_exam"] }
],
"shifts": [
{ "id": "s_frueh", "departmentId": "d1", "name": "Frühdienst",
"startTime": "06:00", "endTime": "14:00", "shiftType": "early", "minStaff": 1 }
],
"config": { "timeout_seconds": 30 }
}' | jq -r .job_id)
# 2) Status pollen
curl -s "$BASE/api/v1/solve/$JOB" -H "X-API-Key: $KEY" | jq .status
# 3) Ergebnis holen
curl -s "$BASE/api/v1/solve/$JOB/result" -H "X-API-Key: $KEY" | jq .
Beispiel-Antwort des Ergebnisses:
{
"success": true,
"status": "optimal",
"assignments": [
{ "employee_id": "e1", "shift_id": "s_frueh", "date": "2026-07-01" }
],
"unassigned_shifts": [],
"execution_time_seconds": 0.8
}
Tutorial: Vom Job zum Plan
Zwei vollständige, real durchgerechnete Beispiele — beide Payloads liefen durch den echten Solver, die Antworten sind sein Ergebnis. Alle Mitarbeiter nur als ID (siehe Datenschutz).
Einfach: drei Mitarbeiter, ein Frühdienst, eine Woche
Ein Frühdienst pro Tag (minStaff: 1), drei Mitarbeiter, eine gesetzliche Regel
(11 Stunden Mindestruhe). Der Solver verteilt die sieben Tage fair.
curl -X POST "$BASE/api/v1/solve" -H "X-API-Key: $KEY" -H "Content-Type: application/json" -d '{
"start_date": "2026-09-01",
"end_date": "2026-09-07",
"department": {
"id": "station-1",
"name": "Pflege Station 1"
},
"employees": [
{
"id": "mitarbeiter-01",
"status": "active",
"primaryDepartmentId": "station-1",
"departmentIds": [
"station-1"
],
"qualificationIds": [],
"contractHoursPerWeek": 40
},
{
"id": "mitarbeiter-02",
"status": "active",
"primaryDepartmentId": "station-1",
"departmentIds": [
"station-1"
],
"qualificationIds": [],
"contractHoursPerWeek": 40
},
{
"id": "mitarbeiter-03",
"status": "active",
"primaryDepartmentId": "station-1",
"departmentIds": [
"station-1"
],
"qualificationIds": [],
"contractHoursPerWeek": 40
}
],
"shifts": [
{
"id": "frueh",
"name": "Frühdienst",
"departmentId": "station-1",
"startTime": "06:00",
"endTime": "14:00",
"shiftType": "early",
"minStaff": 1,
"requiredQualificationIds": [],
"isActive": true
}
],
"planning_rules": [
{
"id": "ruhe-11h",
"name": "ruhe-11h",
"status": "active",
"priority": "legal",
"conditionLogic": "AND",
"conditions": [],
"consequences": [
{
"type": "requireMinRest",
"params": {
"hours": 11
}
}
]
}
],
"config": {
"timeout_seconds": 20
}
}'
Antwort nach dem Abholen (/result):
{
"status": "optimal",
"assignmentCount": 7,
"assignments": [
{
"employee_id": "mitarbeiter-01",
"shift_id": "frueh",
"date": "2026-09-03"
},
{
"employee_id": "mitarbeiter-01",
"shift_id": "frueh",
"date": "2026-09-04"
},
{
"employee_id": "mitarbeiter-01",
"shift_id": "frueh",
"date": "2026-09-05"
},
{
"employee_id": "mitarbeiter-02",
"shift_id": "frueh",
"date": "2026-09-01"
},
{
"employee_id": "mitarbeiter-02",
"shift_id": "frueh",
"date": "2026-09-02"
},
{
"employee_id": "mitarbeiter-03",
"shift_id": "frueh",
"date": "2026-09-06"
}
],
"unassignedShifts": 0,
"executionTimeSeconds": 1.01
}
optimal, nichts unbesetzt — kein Tag ohne
Besetzung, keine Regel verletzt.Komplex: 6 Mitarbeiter, Früh/Spät/Nacht, ein ganzer Monat
Sechs Mitarbeiter (drei examiniert), drei Schichten — der Nachtdienst verlangt eine examinierte Kraft —, ein voller Monat, drei Regeln (Ruhezeit, höchstens 5 Folgetage, max. 10 h/Tag) und ein Wunsch: mitarbeiter-01 will den 3. September frei.
{
"start_date": "2026-09-01",
"end_date": "2026-09-30",
"department": {
"id": "station-1",
"name": "Pflege Station 1"
},
"employees": [
{
"id": "mitarbeiter-01",
"status": "active",
"primaryDepartmentId": "station-1",
"departmentIds": [
"station-1"
],
"qualificationIds": [
"examiniert"
],
"contractHoursPerWeek": 40
},
{
"id": "mitarbeiter-04",
"status": "active",
"primaryDepartmentId": "station-1",
"departmentIds": [
"station-1"
],
"qualificationIds": [],
"contractHoursPerWeek": 40
},
/* … insgesamt 6 Mitarbeiter … */
],
"shifts": [
{
"id": "frueh",
"name": "Frühdienst",
"departmentId": "station-1",
"startTime": "06:00",
"endTime": "14:00",
"shiftType": "early",
"minStaff": 2,
"requiredQualificationIds": [],
"isActive": true
},
{
"id": "spaet",
"name": "Spätdienst",
"departmentId": "station-1",
"startTime": "14:00",
"endTime": "22:00",
"shiftType": "late",
"minStaff": 1,
"requiredQualificationIds": [],
"isActive": true
},
{
"id": "nacht",
"name": "Nachtdienst",
"departmentId": "station-1",
"startTime": "22:00",
"endTime": "06:00",
"shiftType": "night",
"minStaff": 1,
"requiredQualificationIds": [
"examiniert"
],
"isActive": true
}
],
"planning_rules": [
{
"id": "ruhe-11h",
"name": "ruhe-11h",
"status": "active",
"priority": "legal",
"conditionLogic": "AND",
"conditions": [],
"consequences": [
{
"type": "requireMinRest",
"params": {
"hours": 11
}
}
]
},
{
"id": "max-5-folgetage",
"name": "max-5-folgetage",
"status": "active",
"priority": "legal",
"conditionLogic": "AND",
"conditions": [],
"consequences": [
{
"type": "limitConsecutiveDays",
"params": {
"maxDays": 5,
"requiredRestDays": 1
}
}
]
},
{
"id": "max-10h-tag",
"name": "max-10h-tag",
"status": "active",
"priority": "legal",
"conditionLogic": "AND",
"conditions": [],
"consequences": [
{
"type": "limitWorkingHours",
"params": {
"maxHours": 10,
"period": "day"
}
}
]
}
],
"wishes": {
"mitarbeiter-01": [
"2026-09-03"
]
},
"config": {
"timeout_seconds": 45
}
}
→ vollständiger Request (alle 6 Mitarbeiter)
Ergebnis:
{
"status": "optimal",
"assignmentCount": 120,
"assignments": [
{
"employee_id": "mitarbeiter-01",
"shift_id": "frueh",
"date": "2026-09-10"
},
{
"employee_id": "mitarbeiter-01",
"shift_id": "frueh",
"date": "2026-09-11"
},
{
"employee_id": "mitarbeiter-01",
"shift_id": "frueh",
"date": "2026-09-12"
},
{
"employee_id": "mitarbeiter-01",
"shift_id": "frueh",
"date": "2026-09-20"
},
{
"employee_id": "mitarbeiter-01",
"shift_id": "frueh",
"date": "2026-09-21"
},
{
"employee_id": "mitarbeiter-01",
"shift_id": "spaet",
"date": "2026-09-01"
}
],
"unassignedShifts": 0,
"executionTimeSeconds": 8.84
}
optimal. Die Qualifikations-Anforderung der
Nacht wird eingehalten, Folgetage- und Ruhezeit-Regeln greifen — und mitarbeiter-01 hat am
3. September keine Schicht, der Wunsch wurde erfüllt. Wünsche, die mit einer Regel kollidieren,
verlieren gegen die Regel; das Ergebnis bleibt regelkonform.Was, wenn es keine Lösung gibt?
Zu wenig Personal für die geforderte Besetzung? Dann ist status nicht optimal,
sondern infeasible — kein Fehler, sondern die ehrliche Antwort „so geht es nicht". Das Feld
unassigned_shifts nennt die nicht besetzbare Anforderung, damit du die Eingabe anpassen kannst.
Konzepte
Payload-Form
Mitarbeiter, Schichten und Abteilung werden als JSON-Objekte im vigotime-Entitätsformat
(camelCase) übergeben. Pflichtfeld je Entität ist id;
alles Weitere hat sinnvolle Defaults. Daten-Maps wie wishes, vacations oder
sick_leaves haben die Form { "employeeId": ["2026-07-03", …] }.
Asynchrones Job-Modell
POST /solve liefert sofort 202 mit einer job_id und dem aufgelösten
tier. Pollen über GET /solve/{job_id} bis status =
completed (oder failed); dann /result abrufen.
Status-Werte
| Job-Status | Solver-Status (im Ergebnis) |
|---|---|
pending, running, completed, failed, dead |
optimal, feasible, infeasible, unknown, model_invalid |
dead heißt: der Job wurde nach mehreren Fehlversuchen aufgegeben (nicht stille Endlosschleife).
infeasible ist kein Fehler, sondern ein gültiges Ergebnis — es gibt keinen regelkonformen Plan;
unassigned_shifts nennt die nicht besetzbare Anforderung.
Token-Kosten
Jede Aktion kostet Token nach Komplexität — ein Guthaben, keine getrennten Zähler. Nur der Solve skaliert mit der Größe, alles andere ist günstig; Lesezugriffe kosten nichts.
| Operation | Kosten |
|---|---|
solve | 1 + ⌈(MA × Tage × Schichten) / 1000⌉ |
validate, export | 1 (fix) |
Lesezugriffe (GET) | 0 |
Die Kosten stehen vor dem Lauf fest (Problemgröße), sind also vorhersagbar. Jede
Antwort trägt die tatsächlichen Kosten im Header X-Token-Cost; das erledigte Job-Dokument
trägt zusätzlich Metriken (Dauer, Kosten, Komplexität, Ergebnis).
Job-Metriken
Ein abgeschlossener Job dokumentiert sich selbst — im Feld metrics:
{
"durationSeconds": 4.2, // Wall-Clock inkl. Wartezeit
"solverSeconds": 3.8, // reine Rechenzeit
"problemSize": 7750, // MA × Tage × Schichten
"tokenCost": 9,
"solverStatus": "optimal",
"assignments": 412,
"unassignedShifts": 0,
"attempts": 1 // >1 = hat einen Worker-Ausfall überlebt
}
Limits pro Tarif
Der API-Key wird einem Tarif zugeordnet; dieser bestimmt die Grenzen. Die Solver-Laufzeit
wird serverseitig hart gedeckelt (min(config.timeout_seconds, Tarif-Cap)).
| Tarif | max. Problemgröße (MA × Tage × Schichten) | Solver-Zeit | Req/min | parallel |
|---|---|---|---|---|
free | 840 | 15 s | 10 | 1 |
standard | 15 500 | 30 s | 30 | 3 |
pro | 99 200 | 60 s | 60 | 3 |
app (Vollanwendung) | 620 000 | 120 s | 120 | 2 |
enterprise | ∞ | 120 s | 300 | 4 |
422
bevor der Solver startet. Bei Überschreiten des Rate-Limits kommt 429 mit
Retry-After.Guthaben & Abrechnung
Sieh, was du hast und was du ausgegeben hast — jede Abbuchung ist ein Beleg mit Job-Bezug.
Guthaben abfragen
curl "$BASE/api/v1/account/balance" -H "X-API-Key: $KEY"
# → { "balance": 291 }
Verbrauch (Ledger)
Ein append-only Ledger: jede abgerechnete Operation als eigener Beleg — wann, was, wie viel, welcher Job, mit Metadaten. Damit lässt sich eine Rechnung Zeile für Zeile nachrechnen.
curl "$BASE/api/v1/account/usage?limit=50" -H "X-API-Key: $KEY"
{
"count": 2,
"records": [
{
"at": "2026-09-01T10:14:22Z",
"operation": "solve",
"cost": 9,
"balanceAfter": 291,
"reference": "3f9c…", // die Job-ID des Solves
"metadata": { "problemSize": 7750, "tier": "pro" }
},
{
"at": "2026-09-01T09:58:03Z",
"operation": "solve",
"cost": 6,
"balanceAfter": 300,
"reference": "1a2b…",
"metadata": { "problemSize": 4500, "tier": "standard" }
}
]
}
reference verweist auf den Job — von jeder Ledger-Zeile kommst du zurück
zum konkreten Solve und seinen Metriken (Dauer, Komplexität, Ergebnis). balanceAfter ist der
Saldo direkt nach der Abbuchung. Guthaben und Ledger sind an deine Organisation gebunden; ein
Parameter kann den Mandanten nicht wechseln.Fehler
Fehler sind maschinenlesbar: ein stabiler code (der Vertrag — darauf verzweigst du),
die auslösenden Parameter in data, und eine englische Default-Meldung. 402 heißt „Token
kaufen", 429 heißt „warten" — verschiedene Handlungsanweisungen, nie zusammenwerfen.
HTTP/1.1 429 Too Many Requests
Retry-After: 60
{ "detail": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded for tier 'standard' (30/min)",
"data": { "tier": "standard", "limit": 30, "windowSeconds": 60, "retryAfter": 60 }
} }
| Code | HTTP | Bedeutung / was tun |
|---|---|---|
RATE_LIMIT_EXCEEDED | 429 | Rate-Limit — Retry-After abwarten, dann erneut. |
CONCURRENCY_LIMIT | 429 | Zu viele Solves gleichzeitig für den Tarif. Warten. |
INSUFFICIENT_BALANCE | 402 | Token kaufen — erneutes Senden ohne Aufladen scheitert wieder. |
PROBLEM_TOO_LARGE | 422 | Payload über dem Tarif-Limit — Fenster/Team verkleinern oder Tarif hochstufen. |
WEBHOOK_URL_INVALID | 400 | Webhook-Ziel abgelehnt (nur https, keine privaten Hosts). |
JOB_NOT_FOUND | 404 | Unbekannter oder fremder Job (nicht unterscheidbar — Absicht). |
Vollständige API-Referenz
Alle Felder, Schemas und Beispiele interaktiv: