Convenciones del API
Reglas transversales a todos los recursos del v1.
| Tema | Regla |
|---|---|
| Formato | JSON UTF-8. Los requests con cuerpo llevan Content-Type: application/json. |
| IDs | UUID públicos. Nunca se exponen IDs numéricos internos. |
| Fechas | ISO 8601. Instantes en UTC (2026-07-08T09:30:00Z); fechas de calendario YYYY-MM-DD. |
| Base URL | Versionada en el path: https://api.kinmu.app/v1 · Dev: https://api.dev.kinmu.app/v1. |
Paginación (cursor)
Todos los listados paginan por cursor:
?limit=(default 25, máx 100) ·?cursor=<opaco>.- Respuesta:
{ "data": [...], "meta": { "next_cursor": "…"|null, "has_more": true|false } }. - Para la siguiente página, reenvía
cursor=<meta.next_cursor>.has_more=falseonext_cursor=null→ fin.
curl "https://api.kinmu.app/v1/employees?limit=50&cursor=eyJpZCI6MTIzfQ" \
-H "Authorization: Bearer kinmu_sk_live_…"El cursor es opaco: no lo construyas ni lo parsees, reenvíalo tal cual.
Sincronización incremental (updated_since)
El parámetro ?updated_since=<ISO 8601> trae solo lo cambiado desde ese instante (polling incremental para BI y ERPs). No es universal: solo filtra en estos listados.
| Listado | ¿Filtra por updated_since? |
|---|---|
GET /v1/employees | Sí |
GET /v1/check-ins | Sí |
GET /v1/absences | Sí |
GET /v1/locations | Sí |
GET /v1/units | Sí |
GET /v1/webhook-endpoints | Sí |
GET /v1/vacation-balances | No — se acepta pero se ignora; obsoleto, se retira el 27-08-2027 |
GET /v1/work-summaries | No — se acepta pero se ignora; obsoleto, se retira el 27-08-2027 |
curl "https://api.kinmu.app/v1/employees?updated_since=2026-07-01T00:00:00Z" \
-H "Authorization: Bearer kinmu_sk_live_…"Guarda el instante de tu última sincronización y úsalo como updated_since en la siguiente. Ver el patrón completo en la guía de BI.
vacation-balances y work-summaries no filtran por updated_since. Son agregados: no hay un delta que pedir. Vuelve a consultar el rango que te interesa (year= en saldos, from/to en resúmenes) o recárgalo entero en cada pasada, y haz upsert por clave en tu almacén. El parámetro se sigue aceptando en esos dos endpoints por compatibilidad con los SDK ya generados, pero no filtra nada: está marcado como obsoleto y se retirará el 27-08-2027.
Errores
Todos los errores son RFC 9457 (application/problem+json):
{
"type": "https://docs.kinmu.app/errors/invalid_scope",
"title": "Scope insuficiente",
"status": 403,
"code": "invalid_scope",
"detail": "La API key no tiene el scope requerido: org:empleados:read.",
"errors": { "required_scope": "org:empleados:read" }
}Programa contra el campo code (estable), no contra title/detail.
Tabla de códigos
code | HTTP | Cuándo |
|---|---|---|
unauthenticated | 401 | Falta la key, es inválida, revocada o expirada. |
invalid_scope | 403 | La key no tiene el scope requerido (errors.required_scope). |
subscription_inactive | 403 | La empresa no tiene servicio activo: suspendida o archivada, sin suscripción, suscripción suspendida, trial caducado, o suscripción cancelada y vencida o expirada. |
addon_disabled | 403 | El addon Public API no está activo para la empresa. |
validation_failed | 422 | Cuerpo/parámetros inválidos (errors con detalle por campo). |
not_found | 404 | El recurso no existe o pertenece a otra empresa. |
conflict | 409 | Conflicto de estado (p. ej. decidir una ausencia ya decidida). |
idempotency_conflict | 409 | La Idempotency-Key se reusó con un cuerpo distinto. |
rate_limited | 429 | Límite por minuto superado (ver Retry-After). |
quota_exceeded | 429 | Cuota mensual agotada. |
billing_required | 402 | El plan no permite la acción (p. ej. alta de empleado sin asiento). |
internal_error | 500 | Error interno. |
Aislamiento multitenant. Pedir un recurso de otra empresa devuelve 404 (not_found), nunca un 403 que revele su existencia.
Un trial agotado o una cancelación cortan el /v1 con 403 subscription_inactive, nunca con un 402. El 402 (billing_required) es otra cosa: la suscripción sigue viva pero el plan no da para la acción concreta (p. ej. crear un empleado sin asiento libre). Trata el 403 como «no hay servicio, hay que reactivar en el panel» y el 402 como «hay que ampliar el plan».
Revocar API keys y webhooks (DELETE) sigue funcionando con la suscripción inactiva: es un control de seguridad, no consumo del servicio.
Rate limits y cuota
Cada respuesta (2xx y 4xx) incluye, con dos excepciones documentadas más abajo:
| Header | Significado |
|---|---|
X-RateLimit-Limit | Límite por minuto de la key. |
X-RateLimit-Remaining | Peticiones restantes en la ventana actual. |
X-RateLimit-Reset | Timestamp Unix en el que se reinicia la ventana. |
X-Kinmu-Quota-Remaining | Peticiones restantes de la cuota mensual. |
Dos respuestas no llevan estas cabeceras. Los 401 (unauthenticated), porque la autenticación corta antes del throttle y la petición nunca llega a contarse; y todas las respuestas de GET /v1/reports/{report}/download, porque la descarga firmada se sirve fuera del pipeline autenticado. No trates su ausencia como un fallo: en esos dos casos es lo esperado.
Límites: live 120/min, test 30/min. Cuota mensual: live 10.000 + 1.000×empleados_activos (máx 100.000); test 5.000.
Al superar el minuto → 429 rate_limited con Retry-After: <segundos>. Al agotar la cuota → 429 quota_exceeded. Reintenta con backoff respetando Retry-After.
import time, requests
def call_with_retry(session, method, url, **kwargs):
for attempt in range(5):
resp = session.request(method, url, **kwargs)
if resp.status_code != 429:
return resp
wait = int(resp.headers.get("Retry-After", 2 ** attempt))
time.sleep(wait)
resp.raise_for_status()
return respVigila X-RateLimit-Remaining y X-Kinmu-Quota-Remaining para espaciar tus peticiones antes de recibir un 429.
Idempotencia
En métodos que mutan (POST / PATCH / DELETE) puedes enviar Idempotency-Key: <único>:
curl -s -X POST "https://api.kinmu.app/v1/check-ins" \
-H "Authorization: Bearer kinmu_sk_live_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 3f9a…-uuid" \
-d '{ "employee_id": "…", "type": "in", "timestamp": "2026-07-08T08:00:00Z" }'La primera petición se ejecuta y su respuesta se cachea 24 h; un reenvío con el mismo cuerpo devuelve la respuesta original (sin re-ejecutar). Reusar la misma key con un cuerpo distinto → 409 idempotency_conflict.
La creación de webhook es la única excepción: su secreto copy-once nunca se cachea.