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
| Ressource | Liefert |
|---|---|
work-summaries | Arbeitszeitmetriken, fertig für die Aggregation (Stunden, Überstunden, Nachtarbeit). updated_since filtert hier nicht (veraltet): über den Zeitraum neu laden. |
check-ins | Ereignis-Granularität für Anwesenheits- und Pünktlichkeitsanalysen. |
absences | Abwesenheitsquote nach Typ und Zeitraum. |
vacation-balances | Urlaubssalden und Rückstellungen. updated_since filtert hier nicht (veraltet): über year neu laden. |
locations / units | Dimensionen 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
TableZum 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.