vigotime vigotime / developers

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.

REST / JSON Stateless Async Job-Modell Token-Auth (mandantengebunden) Tarif-Limits

Überblick

Drei Endpoints, asynchron: absenden → Status pollen → Ergebnis holen.

POST /api/v1/solve — Job absenden
GET /api/v1/solve/{job_id} — Status
GET /api/v1/solve/{job_id}/result — Ergebnis

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

UmgebungBasis-URL
Productionhttps://api.vigotime.com
Staginghttps://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
Ein Admin deiner Organisation stellt Tokens aus (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).

Schick pseudonyme IDs. Was das Haus verlässt, enthält dann keine personenbezogenen Daten im Sinne der DSGVO — die Zuordnung ID → Person bleibt bei dir. Die Antwort referenziert dieselben IDs zurück (employee_id), du mappst sie lokal auf deine Mitarbeiter.

Konkret: ein employees-Eintrag braucht id, qualificationIds, contractHoursPerWeekkein 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.

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
}
Drei IDs, sieben Frühdienste, 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
}
120 Zuweisungen über den Monat, 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 in der Firestore-Dokument-Form (camelCase) übergeben — dieselbe Form, die unser System intern nutzt. 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-StatusSolver-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.

OperationKosten
solve1 + ⌈(MA × Tage × Schichten) / 1000⌉
validate, export1 (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)).

Tarifmax. Problemgröße
(MA × Tage × Schichten)
Solver-ZeitReq/minparallel
free84015 s101
standard15 50030 s303
pro99 20060 s603
app (Vollanwendung)620 000120 s1202
enterprise120 s3004
Überschreitet ein Payload die Problemgröße, antwortet die API mit 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 }
} }
CodeHTTPBedeutung / was tun
RATE_LIMIT_EXCEEDED429Rate-Limit — Retry-After abwarten, dann erneut.
CONCURRENCY_LIMIT429Zu viele Solves gleichzeitig für den Tarif. Warten.
INSUFFICIENT_BALANCE402Token kaufen — erneutes Senden ohne Aufladen scheitert wieder.
PROBLEM_TOO_LARGE422Payload über dem Tarif-Limit — Fenster/Team verkleinern oder Tarif hochstufen.
WEBHOOK_URL_INVALID400Webhook-Ziel abgelehnt (nur https, keine privaten Hosts).
JOB_NOT_FOUND404Unbekannter oder fremder Job (nicht unterscheidbar — Absicht).
Codes sind additiv: neue kommen jederzeit dazu. Ein Client, der einen unbekannten Code trifft, fällt auf den HTTP-Status zurück. Niemals hart auf einen unbekannten Code scheitern.

Vollständige API-Referenz

Alle Felder, Schemas und Beispiele interaktiv: