API conventions
Rules that apply across all v1 resources.
| Topic | Rule |
|---|---|
| Format | JSON UTF-8. Requests with a body carry Content-Type: application/json. |
| IDs | Public UUIDs. Internal numeric IDs are never exposed. |
| Dates | ISO 8601. Instants in UTC (2026-07-08T09:30:00Z); calendar dates YYYY-MM-DD. |
| Base URL | Versioned in the path: https://api.kinmu.app/v1 · Dev: https://api.dev.kinmu.app/v1. |
Pagination (cursor)
All listings paginate by cursor:
?limit=(default 25, max 100) ·?cursor=<opaque>.- Response:
{ "data": [...], "meta": { "next_cursor": "…"|null, "has_more": true|false } }. - For the next page, resend
cursor=<meta.next_cursor>.has_more=falseornext_cursor=null→ end.
curl "https://api.kinmu.app/v1/employees?limit=50&cursor=eyJpZCI6MTIzfQ" \
-H "Authorization: Bearer kinmu_sk_live_…"The cursor is opaque: don’t build it or parse it, resend it as-is.
Incremental sync (updated_since)
All listings accept ?updated_since=<ISO 8601> to fetch only what changed since that instant (incremental polling for BI and ERPs).
curl "https://api.kinmu.app/v1/employees?updated_since=2026-07-01T00:00:00Z" \
-H "Authorization: Bearer kinmu_sk_live_…"Store the instant of your last sync and use it as updated_since on the next one. See the full pattern in the BI guide.
Errors
All errors are RFC 9457 (application/problem+json):
{
"type": "https://docs.kinmu.app/errors/invalid_scope",
"title": "Insufficient scope",
"status": 403,
"code": "invalid_scope",
"detail": "The API key does not have the required scope: org:empleados:read.",
"errors": { "required_scope": "org:empleados:read" }
}Program against the code field (stable), not against title/detail.
Code table
code | HTTP | When |
|---|---|---|
unauthenticated | 401 | Missing key, or the key is invalid, revoked or expired. |
invalid_scope | 403 | The key lacks the required scope (errors.required_scope). |
subscription_inactive | 403 | The company’s subscription is suspended/absent. |
addon_disabled | 403 | The Public API addon is not active for the company. |
validation_failed | 422 | Invalid body/parameters (errors with per-field detail). |
not_found | 404 | The resource does not exist or belongs to another company. |
conflict | 409 | State conflict (e.g. deciding an already-decided absence). |
idempotency_conflict | 409 | The Idempotency-Key was reused with a different body. |
rate_limited | 429 | Per-minute limit exceeded (see Retry-After). |
quota_exceeded | 429 | Monthly quota exhausted. |
billing_required | 402 | The plan does not allow the action (e.g. adding an employee without a seat). |
internal_error | 500 | Internal error. |
Multitenant isolation. Requesting a resource from another company returns 404 (not_found), never a 403 that would reveal its existence.
Rate limits and quota
Every response (2xx and 4xx) includes:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | The key’s per-minute limit. |
X-RateLimit-Remaining | Requests remaining in the current window. |
X-RateLimit-Reset | Unix timestamp at which the window resets. |
X-Kinmu-Quota-Remaining | Requests remaining in the monthly quota. |
Limits: live 120/min, test 30/min. Monthly quota: live 10,000 + 1,000×active_employees (max 100,000); test 5,000.
When you exceed the minute → 429 rate_limited with Retry-After: <seconds>. When you exhaust the quota → 429 quota_exceeded. Retry with backoff, respecting 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 respWatch X-RateLimit-Remaining and X-Kinmu-Quota-Remaining to space out your requests before getting a 429.
Idempotency
On mutating methods (POST / PATCH / DELETE) you can send Idempotency-Key: <unique>:
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" }'The first request executes and its response is cached for 24 h; a resend with the same body returns the original response (without re-executing). Reusing the same key with a different body → 409 idempotency_conflict.
Webhook creation is the only exception: its copy-once secret is never cached.