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)
Todos los listados aceptan ?updated_since=<ISO 8601> para traer solo lo cambiado desde ese instante (polling incremental para BI y ERPs).
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.
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 suscripción de la empresa está suspendida/ausente. |
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.
Rate limits y cuota
Cada respuesta (2xx y 4xx) incluye:
| 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. |
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.