BI / Power BI
Objetivo: levar os dados de controle de ponto para o seu data warehouse ou diretamente para o Power BI / Looker / Metabase com sincronização incremental.
Scopes recomendados (somente leitura): org:fichajes:read, org:ausencias:read, org:saldos:read, org:estructura:read.
Para BI, use uma chave dedicada somente de leitura. Assim você pode revogá-la ou rotacioná-la sem afetar integrações de escrita.
Padrão: polling incremental com updated_since
updated_since=<ISO8601> devolve apenas o que foi modificado desde esse instante, mas nem todas as listagens o aceitam: employees, check-ins, absences, locations, units e webhook-endpoints sim; em vacation-balances e work-summaries é aceite mas não filtra (obsoleto, remoção a 27-08-2027). O padrão:
Carga inicial (backfill)
Percorra cada recurso paginando por cursor até has_more=false. Guarde o instante de início como marca-d’água (watermark).
Cargas incrementais
Em cada execução agendada, peça ?updated_since=<watermark> e atualize sua marca-d’água para o instante anterior ao início da chamada.
Deduplique por id
Como updated_since se baseia em updated_at, um mesmo registro pode aparecer de novo se mudou. Faça upsert por id (UUID) no seu armazenamento.
Recarregue os agregados por intervalo
vacation-balances e work-summaries não filtram por updated_since: são agregados, não há delta para pedir. Em cada passagem, volte a consultar o intervalo que lhe interessa — year= nos saldos, from/to nos resumos — e faça upsert pela chave real de cada recurso: (employee_id, period_start, period_end) em work-summaries e (employee_id, year) em vacation-balances. Para um fecho mensal, basta recarregar o mês em curso e o anterior.
Exemplo em 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) # início do histórico que lhe interessa
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)
# Marca de água ANTES de chamar, para não perder registos escritos durante a 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)
# A janela `from`/`to` delimita QUE eventos lhe interessam (o `timestamp` da picagem).
# O `updated_since` delimita QUE ALTERAÇÕES trazer (`updated_at`): os troços sem alterações voltam vazios.
# São eixos diferentes, por isso o intervalo arranca sempre em SYNC_START: uma correção feita hoje
# numa picagem de janeiro só chega se o troço de janeiro continuar dentro da janela.
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)
# Agregados: sem `updated_since` recarrega-se o intervalo inteiro. Começa no primeiro dia
# do MÊS ANTERIOR (em janeiro passa para o ano anterior) para apanhar consolidações
# tardias do fecho anterior.
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)Capture a marca-d’água antes de começar a sincronização, não depois. Assim os registros escritos enquanto o processo rodava entram na próxima passada.
Recursos úteis para BI
| Recurso | Traz |
|---|---|
work-summaries | Métricas de jornada prontas para agregação (horas, extras, noturnas). updated_since não filtra aqui (obsoleto): recarga por intervalo. |
check-ins | Grão de evento para análise de presença e pontualidade. |
absences | Absenteísmo por tipo e período. |
vacation-balances | Saldos e provisões de férias. updated_since não filtra aqui (obsoleto): recarga por year. |
locations / units | Dimensões para segmentar (local, departamento). |
Conexão a partir do Power BI
O Power BI pode consumir a API diretamente com Web.Contents e header de autorização. Exemplo simplificado em 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
TablePara paginar no Power Query, envolva a chamada em uma função que siga meta.next_cursor com List.Generate até has_more ser false.
Respeite os rate limits: para volumes grandes, agende a atualização fora dos horários de pico e acompanhe X-Kinmu-Quota-Remaining.
Alternativa event-driven
Se preferir não fazer polling, inscreva-se em webhooks (checkin.created, absence.approved, vacation_balance.updated, …) e atualize seu armazenamento a cada evento recebido. Os dois combinam bem: webhooks para tempo real + um polling noturno com updated_since como rede de segurança.