Grundlagen
Beta Die REST-API ist in der Beta. Endpunkte und Felder können sich bis zur Version 1.0.0 noch ändern.
Diese Regeln gelten für alle Endpunkte der TimeMonkey REST-API.
Requests und Antworten
- Die API ist rein lesend und nimmt nur
GET-Requests an. Andere HTTP-Methoden beantwortet sie mit 405method_not_allowedund dem HeaderAllow: GET. - Alle Antworten sind JSON, auch Fehler. Die Fehlerform steht unter Fehler und Rate-Limits.
- Antworten unter
/api/v1tragen den HeaderX-Request-Id, auch Fehlerantworten wie 404 und 405. Gib diese ID an, wenn du dich wegen eines bestimmten Requests an den Support wendest. - Antworten werden nicht zwischengespeichert (
Cache-Control: no-store). Jeder Request liefert den aktuellen Stand. - Unbekannte Abfrageparameter sind ein Fehler: Die API antwortet dann mit 400
validation_error, statt den Parameter stillschweigend zu ignorieren. Achte deshalb auf die genaue Schreibweise, zum BeispielemployeeId. - Unbekannte Pfade unter
/api/v1beantwortet die API mit 404not_found.
Einzige Ausnahme ist GET /api/v1/openapi.json: Die Spezifikation ist statisch, darf zwischengespeichert werden und hat keine X-Request-Id.
IDs
Alle IDs sind 24-stellige Hexadezimal-Zeichenketten, zum Beispiel 650a1b2c3d4e5f6071829304. Über sie verknüpfst du die Daten der Endpunkte: Das Feld employeeId eines Stempels entspricht zum Beispiel dem Feld id des Mitarbeiters aus GET /api/v1/employees. Eine ID in falschem Format, etwa als Pfad- oder Filterparameter, führt zu 400 validation_error.
Zeitpunkte
Zeitpunkte wie Stempelzeiten oder Schichtbeginn liefert die API im Format ISO 8601 in UTC mit Millisekunden, erkennbar am Z am Ende:
"timestamp": "2026-10-01T05:58:12.000Z"
Das entspricht 07:58:12 Uhr deutscher Sommerzeit. Rechne Zeitpunkte für die Anzeige selbst in die Zeitzone Europe/Berlin um, damit Sommer- und Winterzeit richtig berücksichtigt werden.
Datumsangaben und Zeitzone
TimeMonkey rechnet alle Kalendertage in der Zeitzone Europe/Berlin. Das gilt für Filter und für Datumsfelder in Antworten. Das Feld timeZone von GET /api/v1/company enthält deshalb immer "Europe/Berlin".
- Datumsfilter wie
fromundtogibst du im FormatYYYY-MM-DDan. Beide Grenzen sind inklusive:from=2026-10-01&to=2026-10-31umfasst den 1. Oktober ab 00:00 Uhr bis einschließlich 31. Oktober, 24:00 Uhr, jeweils deutscher Zeit. - Ungültige Kalenderdaten wie
2026-02-30und eintovorfromführen zu 400validation_error. - Datumsfelder in Antworten, etwa
dateder Tagesauswertung oderhalfDaysbei Abwesenheiten, haben ebenfalls das FormatYYYY-MM-DDund beziehen sich auf Kalendertage in Europe/Berlin.
Pro Request ist der Zeitraum begrenzt, damit Abfragen schnell bleiben:
| Endpunkt | from und to | Längster Zeitraum pro Request |
|---|---|---|
GET /api/v1/stamps | Pflicht | 366 Tage |
GET /api/v1/shifts | Pflicht | 366 Tage |
GET /api/v1/evaluations/days | Pflicht | 92 Tage, to nicht in der Zukunft |
GET /api/v1/absences | optional | keine Begrenzung |
Brauchst du einen längeren Zeitraum, teile ihn auf mehrere Requests auf, zum Beispiel monatsweise.
Dauern
Alle Dauern liefert die API in Millisekunden als Zahl. Felder mit Dauern enden auf Ms, zum Beispiel workedMs oder weeklyMs.
| Dauer | Millisekunden |
|---|---|
| 1 Minute | 60000 |
| 1 Stunde | 3600000 |
| 8 Stunden | 28800000 |
| 38,5 Stunden | 138600000 |
Stunden erhältst du, indem du durch 3.600.000 teilst: 28800000 / 3600000 = 8. Ausnahmen sind annualVacationDays bei Mitarbeitern und das Urlaubskonto vacation der Jahresauswertung: Urlaub wird in Tagen angegeben.
Salden und Summen der Auswertungen können bei Monatsverträgen Nachkommastellen haben, weil TimeMonkey die Monatsstunden auf die Tage verteilt. Runde erst bei der Anzeige, nicht vor dem Weiterrechnen.
Pagination
Listen-Endpunkte liefern ihre Ergebnisse seitenweise in dieser Form:
{
"data": [ ... ],
"nextCursor": "WzE3OTA5MjA4MDAwMDAsIjY3MjBmMGExYjJjM2Q0ZTVmNmE3YjgwMyJd"
}
- Mit
limitlegst du fest, wie viele Einträge eine Seite höchstens enthält: 1 bis 500, Standard 100. Die Auswertungs-Endpunkte können eine Seite auch früher beenden, weil ihre Rechenzeit pro Seite begrenzt ist. Eine Seite mit weniger Einträgen ist also nicht automatisch die letzte. - Ist
nextCursornichtnull, gibt es weitere Einträge. Übergib den Wert beim nächsten Request als Parametercursorund lass die übrigen Parameter unverändert. - Erst wenn
nextCursorgleichnullist, hast du die letzte Seite erreicht. - Der Cursor ist eine undurchsichtige Zeichenkette: Verwende ihn genau so, wie du ihn bekommen hast, und setze ihn nicht selbst zusammen. Ein veränderter oder ungültiger Cursor führt zu 400
validation_error.
GET /api/v1/areas ist nicht paginiert und liefert nur data. GET /api/v1/company und GET /api/v1/employees/{id} liefern ein einzelnes Objekt.
Die Einträge einer Liste kommen in fester Reihenfolge:
| Endpunkt | Sortierung |
|---|---|
GET /api/v1/employees | nach id |
GET /api/v1/stamps | nach timestamp, dann id |
GET /api/v1/absences | nach start, dann id |
GET /api/v1/shifts | nach start, dann id |
GET /api/v1/evaluations/days | nach employeeId, dann date |
GET /api/v1/evaluations/months | nach employeeId, dann month |
GET /api/v1/evaluations/years | nach employeeId |
Alle Seiten abrufen
Jede Seite ist ein eigener Request und zählt für das Rate-Limit. Mit limit=500 brauchst du die wenigsten Requests. Rufe zuerst die erste Seite ab und übergib dann deren nextCursor als cursor für die nächste Seite; lass die übrigen Parameter unverändert. Ersetze im curl-Beispiel <nextCursor> durch den Wert aus der vorherigen Antwort. Sobald nextCursor null ist, hast du alle Seiten.
- curl
- JavaScript
- Python
curl -s "https://api.time.monkeymed.de/api/v1/stamps?from=2026-10-01&to=2026-10-31&limit=500" \
-H "Authorization: Bearer $TIMEMONKEY_API_KEY"
curl -s "https://api.time.monkeymed.de/api/v1/stamps?from=2026-10-01&to=2026-10-31&limit=500&cursor=<nextCursor>" \
-H "Authorization: Bearer $TIMEMONKEY_API_KEY"
const BASE_URL = 'https://api.time.monkeymed.de/api/v1';
const headers = {Authorization: `Bearer ${process.env.TIMEMONKEY_API_KEY}`};
async function fetchAll(path, params) {
const items = [];
let cursor = null;
do {
const query = new URLSearchParams({...params, limit: '500'});
if (cursor) query.set('cursor', cursor);
const response = await fetch(`${BASE_URL}${path}?${query}`, {headers});
const body = await response.json();
if (!response.ok) {
throw new Error(`${response.status} ${body.error.code}: ${body.error.message}`);
}
items.push(...body.data);
cursor = body.nextCursor;
} while (cursor);
return items;
}
const stamps = await fetchAll('/stamps', {from: '2026-10-01', to: '2026-10-31'});
console.log(`${stamps.length} Stempel geladen`);
import os
import requests
BASE_URL = "https://api.time.monkeymed.de/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['TIMEMONKEY_API_KEY']}"}
def fetch_all(path, params):
items, cursor = [], None
while True:
query = {**params, "limit": 500}
if cursor:
query["cursor"] = cursor
response = requests.get(BASE_URL + path, headers=HEADERS, params=query, timeout=30)
body = response.json()
if not response.ok:
raise RuntimeError(f"{response.status_code} {body['error']['code']}: {body['error']['message']}")
items.extend(body["data"])
cursor = body["nextCursor"]
if cursor is None:
return items
stamps = fetch_all("/stamps", {"from": "2026-10-01", "to": "2026-10-31"})
print(f"{len(stamps)} Stempel geladen")