Skip to Content

BI / Power BI

Objectif : amener les données de suivi du temps dans votre data warehouse ou directement dans Power BI / Looker / Metabase via une synchronisation incrémentale.

Scopes recommandés (lecture seule) : org:fichajes:read, org:ausencias:read, org:saldos:read, org:estructura:read.

Pour la BI, utilisez une clé dédiée en lecture seule. Vous pourrez ainsi la révoquer ou la faire tourner sans impacter vos intégrations en écriture.

Pattern : polling incrémental avec updated_since

updated_since=<ISO8601> ne renvoie que ce qui a été modifié depuis cet instant, mais toutes les listes ne l’acceptent pas : employees, check-ins, absences, locations, units et webhook-endpoints oui ; sur vacation-balances et work-summaries il est accepté mais ne filtre pas (obsolète, retrait le 27/08/2027). Le pattern :

Chargement initial (backfill)

Parcourez chaque ressource en paginant par cursor jusqu’à has_more=false. Conservez l’instant de départ comme marque d’eau (watermark).

Chargements incrémentaux

À chaque exécution planifiée, appelez ?updated_since=<watermark> et mettez à jour votre marque d’eau avec l’instant précédant le début de l’appel.

Dédupliquez par id

Comme updated_since s’appuie sur updated_at, un même enregistrement peut réapparaître s’il a changé. Faites un upsert par id (UUID) dans votre entrepôt.

Rechargez les agrégats par plage

vacation-balances et work-summaries ne filtrent pas par updated_since : ce sont des agrégats, il n’y a pas de delta à demander. Redemandez la plage qui vous intéresse à chaque passe — year= pour les soldes, from/to pour les résumés — et faites un upsert sur la vraie clé de chaque ressource : (employee_id, period_start, period_end) pour work-summaries et (employee_id, year) pour vacation-balances. Pour une clôture mensuelle, recharger le mois en cours et le précédent suffit.

Exemple en 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) # début de l'historique qui vous intéresse 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) # Filigrane AVANT l'appel, pour ne pas perdre les enregistrements écrits pendant la sync 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) # La fenêtre `from`/`to` borne QUELS événements vous intéressent (le `timestamp` du pointage). # `updated_since` borne QUELS CHANGEMENTS récupérer (`updated_at`) : les tranches sans changement reviennent vides. # Ce sont deux axes distincts : la plage part donc toujours de SYNC_START, car une correction faite # aujourd'hui sur un pointage de janvier n'arrive que si la tranche de janvier est encore dans la fenêtre. 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) # Agrégats : sans `updated_since`, on recharge toute la plage. On démarre au premier jour # du MOIS PRÉCÉDENT (en janvier, cela bascule sur l'année passée) pour récupérer les # consolidations tardives de la clôture précédente. 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)

Prenez la marque d’eau avant de démarrer la synchronisation, pas après. Les enregistrements écrits pendant l’exécution seront ainsi repris à la passe suivante.

Ressources utiles pour la BI

RessourceApporte
work-summariesMétriques de temps de travail prêtes à agréger (heures, supplémentaires, de nuit). updated_since ne filtre pas ici (obsolète) : rechargement par plage.
check-insGrain événement pour l’analyse de présence et de ponctualité.
absencesAbsentéisme par type et par période.
vacation-balancesSoldes et provisions de congés. updated_since ne filtre pas ici (obsolète) : rechargement par year.
locations / unitsDimensions de segmentation (site, département).

Connexion depuis Power BI

Power BI peut consommer l’API directement avec Web.Contents et le header d’autorisation. Exemple simplifié en 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

Pour paginer en Power Query, encapsulez l’appel dans une fonction qui suit meta.next_cursor avec List.Generate jusqu’à ce que has_more vaille false.

Respectez les limites de débit : pour les gros volumes, planifiez l’actualisation hors des heures de pointe et surveillez X-Kinmu-Quota-Remaining.

Alternative event-driven

Si vous préférez éviter le polling, abonnez-vous aux webhooks (checkin.created, absence.approved, vacation_balance.updated, …) et mettez à jour votre entrepôt à la réception de chaque événement. Les deux approches se combinent bien : webhooks pour le temps réel + un polling nocturne avec updated_since en filet de sécurité.

Last updated on