Skip to Content
Conventions

API conventions

Rules that apply across all v1 resources.

TopicRule
FormatJSON UTF-8. Requests with a body carry Content-Type: application/json.
IDsPublic UUIDs. Internal numeric IDs are never exposed.
DatesISO 8601. Instants in UTC (2026-07-08T09:30:00Z); calendar dates YYYY-MM-DD.
Base URLVersioned 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=false or next_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

codeHTTPWhen
unauthenticated401Missing key, or the key is invalid, revoked or expired.
invalid_scope403The key lacks the required scope (errors.required_scope).
subscription_inactive403The company’s subscription is suspended/absent.
addon_disabled403The Public API addon is not active for the company.
validation_failed422Invalid body/parameters (errors with per-field detail).
not_found404The resource does not exist or belongs to another company.
conflict409State conflict (e.g. deciding an already-decided absence).
idempotency_conflict409The Idempotency-Key was reused with a different body.
rate_limited429Per-minute limit exceeded (see Retry-After).
quota_exceeded429Monthly quota exhausted.
billing_required402The plan does not allow the action (e.g. adding an employee without a seat).
internal_error500Internal 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:

HeaderMeaning
X-RateLimit-LimitThe key’s per-minute limit.
X-RateLimit-RemainingRequests remaining in the current window.
X-RateLimit-ResetUnix timestamp at which the window resets.
X-Kinmu-Quota-RemainingRequests 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 resp

Watch 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 body409 idempotency_conflict.

Webhook creation is the only exception: its copy-once secret is never cached.

Last updated on