Skip to Content

BI / Power BI

Ziel: die Zeiterfassungsdaten in dein Data Warehouse oder direkt nach Power BI / Looker / Metabase bringen — per inkrementeller Synchronisierung.

Empfohlene Scopes (nur lesend): org:fichajes:read, org:ausencias:read, org:saldos:read, org:estructura:read.

Verwende für BI einen dedizierten Read-only-Key. So kannst du ihn widerrufen oder rotieren, ohne Schreibintegrationen zu beeinträchtigen.

Pattern: inkrementelles Polling mit updated_since

updated_since=<ISO8601> liefert nur, was sich seit diesem Zeitpunkt geändert hat — aber nicht alle Listen akzeptieren den Parameter: employees, check-ins, absences, locations, units und webhook-endpoints ja; bei vacation-balances und work-summaries wird er zwar akzeptiert, filtert aber nicht (veraltet, Entfernung am 27.08.2027). Das Pattern:

Erstladung (Backfill)

Gehe jede Ressource durch und paginiere per cursor, bis has_more=false. Speichere den Startzeitpunkt als Wasserzeichen (watermark).

Inkrementelle Läufe

Frage bei jedem geplanten Lauf ?updated_since=<watermark> an und setze dein Wasserzeichen auf den Zeitpunkt vor Beginn des Aufrufs.

Dedupliziere über id

Da updated_since auf updated_at basiert, kann derselbe Datensatz erneut auftauchen, wenn er sich geändert hat. Mache in deinem Speicher ein Upsert über die id (UUID).

Lade die Aggregate über den Zeitraum neu

vacation-balances und work-summaries filtern nicht nach updated_since: Es sind Aggregate, es gibt kein Delta. Frage bei jedem Lauf den Zeitraum erneut ab — year= bei Salden, from/to bei Summaries — und mache ein Upsert über den echten Schlüssel der jeweiligen Ressource: (employee_id, period_start, period_end) bei work-summaries und (employee_id, year) bei vacation-balances. Für einen Monatsabschluss reicht es, den laufenden und den vorherigen Monat neu zu laden.

Beispiel in Python

import os, requests from datetime import date, datetime, timedelta, timezone BASE = os.environ["KINMU_BASE_URL"] KEY = os.environ["KINMU_API_KEY"] SYNC_START = date(2026, 1, 1) # Beginn der Historie, die dich interessiert session = requests.Session() session.headers.update({"Authorization": f"Bearer {KEY}"}) def sync(resource, since=None): rows, cursor = [], None params = {"limit": 100} if since: params["updated_since"] = since while True: if cursor: params["cursor"] = cursor r = session.get(f"{BASE}/{resource}", params=params, timeout=30) r.raise_for_status() body = r.json() rows.extend(body["data"]) if not body["meta"]["has_more"]: break cursor = body["meta"]["next_cursor"] return rows def first_day_of_previous_month(day): first = day.replace(day=1) return (first - timedelta(days=1)).replace(day=1) # Wasserzeichen VOR dem Aufruf, damit während der Sync geschriebene Datensätze nicht verloren gehen watermark = datetime.now(timezone.utc).isoformat() today = datetime.now(timezone.utc).date() last_watermark = load_last_watermark() # ISO 8601 | None employees = sync("employees", since=last_watermark) # Das Fenster `from`/`to` grenzt ein, WELCHE Ereignisse dich interessieren (der `timestamp` der Stempelung). # `updated_since` grenzt ein, WELCHE ÄNDERUNGEN geholt werden (`updated_at`): Abschnitte ohne Änderung kommen leer zurück. # Zwei verschiedene Achsen — deshalb startet der Zeitraum immer bei SYNC_START: Eine heutige Korrektur # an einer Januar-Stempelung kommt nur an, wenn der Januar-Abschnitt noch im Fenster liegt. checkins = [] chunk_from = SYNC_START while chunk_from <= today: chunk_to = min(chunk_from + timedelta(days=91), today) checkins += sync(f"check-ins?from={chunk_from}&to={chunk_to}", since=last_watermark) chunk_from = chunk_to + timedelta(days=1) # Aggregate: ohne `updated_since` wird der ganze Zeitraum neu geladen. Start am ersten Tag # des VORMONATS (im Januar also im Vorjahr), um späte Konsolidierungen des vorherigen # Abschlusses noch mitzunehmen. previous_month = first_day_of_previous_month(today) balances = [] for year in sorted({previous_month.year, today.year}): balances += sync(f"vacation-balances?year={year}") summaries = sync(f"work-summaries?period=month&from={previous_month}&to={today}") save_watermark(watermark)

Nimm das Wasserzeichen vor dem Start der Synchronisierung, nicht danach. So landen Datensätze, die während des Laufs geschrieben wurden, im nächsten Durchgang.

Nützliche Ressourcen für BI

RessourceLiefert
work-summariesArbeitszeitmetriken, fertig für die Aggregation (Stunden, Überstunden, Nachtarbeit). updated_since filtert hier nicht (veraltet): über den Zeitraum neu laden.
check-insEreignis-Granularität für Anwesenheits- und Pünktlichkeitsanalysen.
absencesAbwesenheitsquote nach Typ und Zeitraum.
vacation-balancesUrlaubssalden und Rückstellungen. updated_since filtert hier nicht (veraltet): über year neu laden.
locations / unitsDimensionen zum Segmentieren (Standort, Abteilung).

Anbindung aus Power BI

Power BI kann die API direkt mit Web.Contents und Authorization-Header konsumieren. Vereinfachtes Beispiel in Power Query (M):

let BaseUrl = "https://api.kinmu.app/v1", ApiKey = "kinmu_sk_live_…", // usa Parámetros / almacén de credenciales, no lo escribas en claro Source = Json.Document( Web.Contents(BaseUrl, [ RelativePath = "work-summaries", Query = [ period = "month", from = "2026-01-01", #"to" = "2026-12-31", limit = "100" ], Headers = [ Authorization = "Bearer " & ApiKey, Accept = "application/json" ] ]) ), Data = Source[data], Table = Table.FromRecords(Data) in Table

Zum Paginieren in Power Query kapselst du den Aufruf in eine Funktion, die meta.next_cursor mit List.Generate folgt, bis has_more gleich false ist.

Respektiere die Rate Limits: Plane die Aktualisierung bei großen Volumina außerhalb der Stoßzeiten und behalte X-Kinmu-Quota-Remaining im Blick.

Event-getriebene Alternative

Wenn du nicht pollen willst, abonniere Webhooks (checkin.created, absence.approved, vacation_balance.updated, …) und aktualisiere deinen Speicher bei jedem Event. Beides kombiniert sich gut: Webhooks für Echtzeit + ein nächtliches Polling mit updated_since als Sicherheitsnetz.

Last updated on