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)

The ?updated_since=<ISO 8601> parameter fetches only what changed since that instant (incremental polling for BI and ERPs). It is not universal: it only filters on these listings.

ListingFilters by updated_since?
GET /v1/employeesYes
GET /v1/check-insYes
GET /v1/absencesYes
GET /v1/locationsYes
GET /v1/unitsYes
GET /v1/webhook-endpointsYes
GET /v1/vacation-balancesNo — accepted but ignored; deprecated, removal on 2027-08-27
GET /v1/work-summariesNo — accepted but ignored; deprecated, removal on 2027-08-27
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.

vacation-balances and work-summaries do not filter by updated_since. They are aggregates: there is no delta to ask for. Re-query the range you care about (year= for balances, from/to for summaries) or reload it whole on every pass, and upsert by key in your store. The parameter is still accepted on those two endpoints for compatibility with already-generated SDKs, but it filters nothing: it is marked deprecated and will be removed on 2027-08-27.

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 has no active service: suspended or archived, no subscription, suspended subscription, expired trial, or a canceled and lapsed or expired subscription.
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.

An exhausted trial or a cancellation cut off /v1 with 403 subscription_inactive, never with a 402. The 402 (billing_required) is a different thing: the subscription is alive but the plan does not cover the specific action (e.g. creating an employee with no seat left). Read the 403 as “no service, reactivate in the dashboard” and the 402 as “upgrade the plan”.

Revoking API keys and webhooks (DELETE) keeps working while the subscription is inactive: it is a security control, not service usage.

Rate limits and quota

Every response (2xx and 4xx) includes, with two exceptions documented below:

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.

Two responses do not carry these headers. 401 (unauthenticated), because authentication cuts in before the throttle and the request is never counted; and every response of GET /v1/reports/{report}/download, because the signed download is served outside the authenticated pipeline. Don’t treat their absence as a failure: in those two cases it is expected.

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