Hinatadocs

API reference

Hinata exposes a stable, versioned REST API under /api/v1. The app uses this same API for everything, from projects and issues to boards, sprints and the knowledge base. So your own scripts and integrations can do anything the client can.

This page is an orientation. It covers the rules that apply everywhere. The full, always current endpoint list is in the Scalar docs UI.

Base URL and versioning

All endpoints live under the /api/v1 prefix on your server's public API host:

https://api.track.example.com/api/v1

v1 is the contract version. Breaking changes would ship under a new prefix, so you can pin to v1 safely. In the app you configure this base per server. In your own clients, append the paths below to https://api.track.example.com/api/v1.

Authentication model

Hinata uses stateless JWTs (HS512) with two kinds of token:

TokenLifetimeWhat it is for
Access tokenShort-livedThe Bearer credential you send on every authenticated request.
Refresh tokenLonger-livedUsed only to mint a new access token via /auth/refresh.

Put the access token in the Authorization header:

Authorization: Bearer <access-token>

Refresh tokens are rejected for API access

A refresh token can only be exchanged for a new access token at /auth/refresh. No other endpoint accepts it as a Bearer credential. Send it as Authorization: Bearer … to, say, /issues and the request is rejected. Always call authenticated endpoints with a fresh access token.

When an access token expires, exchange your refresh token for a new one instead of logging in again:

curl -sS -X POST https://api.track.example.com/api/v1/auth/refresh \
  -H 'Content-Type: application/json' \
  -d '{"refreshToken":"<refresh-token>"}'

The app does this automatically. Its ApiClient catches a 401, calls /auth/refresh, swaps in the new access token and retries the original request once. See Authentication for the full token model.

Localized errors with Accept-Language

Send an Accept-Language header (en or de) and you get error messages in that language. The server resolves them from resource bundles, so the client needs no translation logic:

curl -sS https://api.track.example.com/api/v1/projects \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Accept-Language: de'

Errors are stable, machine-readable JSON with a human message already in the requested language. They never include stack traces.

Public endpoints (no token)

These endpoints work without a Bearer token. The app needs them before anyone has signed in: to discover the server, check setup status and log in.

MethodEndpointPurpose
GET/metaServer metadata: minimum app version, privacy URL, auth feature flags.
GET/setup/statusWhether first-run setup has been completed.
POST/setupComplete first-run setup (organization + first admin).
POST/auth/loginExchange credentials for access + refresh tokens.
POST/auth/refreshExchange a refresh token for a new access token.
GET/auth/sso/providersList the configured SSO providers (OIDC, OAuth2, SAML, LDAP).
GET/actuator/healthLiveness/health probe for load balancers and uptime checks.

Everything else needs a Bearer token

Any path not in the table above requires a valid access token. Admin routes under /api/v1/admin/** additionally require the ADMIN role.

Log in, then call the API

The everyday flow: POST /auth/login to get tokens, then send the access token as a Bearer credential on every following call.

1. Log in and receive an access token and a refresh token:

curl -sS -X POST https://api.track.example.com/api/v1/auth/login \
  -H 'Content-Type: application/json' \
  -H 'Accept-Language: en' \
  -d '{"usernameOrEmail":"rebar","password":"your-password"}'

The response contains the tokens (field names may include accessToken and refreshToken) plus the signed-in user. Copy the access token.

2. Call an authenticated endpoint with that token as a Bearer credential:

curl -sS https://api.track.example.com/api/v1/projects \
  -H 'Authorization: Bearer <access-token>'

Capture the token in one step

With jq you can log in and keep the access token in a shell variable:

TOKEN=$(curl -sS -X POST https://api.track.example.com/api/v1/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"usernameOrEmail":"rebar","password":"your-password"}' \
  | jq -r '.accessToken')

curl -sS https://api.track.example.com/api/v1/projects \
  -H "Authorization: Bearer $TOKEN"

If TOTP two-factor is enabled for the account, /auth/login returns a 2FA challenge instead of tokens. Complete the challenge to receive them. See Authentication.

Live updates with Server-Sent Events

Some resources push changes to connected clients over Server-Sent Events (SSE), so you don't have to poll. Attachments are the clearest example: when a file is added to or removed from an issue, every client streaming that issue is notified immediately at:

GET /api/v1/issues/{issueId}/attachments/stream

Open the stream with curl. The -N flag disables buffering so events print as they arrive:

curl -N https://api.track.example.com/api/v1/issues/ASTA-42/attachments/stream \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Accept: text/event-stream'

The connection stays open and emits an event each time the issue's attachments change. SSE is a one-way, long-lived HTTP stream with no WebSocket upgrade.

Disable proxy buffering for SSE

A reverse proxy that buffers responses holds SSE events back until the connection closes, which looks like "live updates don't work." Turn buffering off for the stream path (for example proxy_buffering off; on nginx). See Reverse proxy & TLS and the FAQ.

Rate limiting

The API is rate-limited per client IP with bucket4j. Authentication routes get a strict budget against brute force:

ScopeDefault limitEnvironment variable
General API300 requests/minuteHINATA_RATE_LIMIT_API
/auth/**10 requests/minuteHINATA_RATE_LIMIT_AUTH

GET /auth/sso/providers is the one exception and counts against the general budget. The sign-in screen calls it on every visit to decide which SSO buttons to draw, and it exposes nothing an attacker could guess at.

HINATA_RATE_LIMIT_ENABLED toggles rate limiting (on by default). Repeated failed logins also trigger a database-backed lockout that survives restarts:

  • HINATA_MAX_LOGIN_FAILURES, default 5
  • HINATA_LOGIN_BLOCK_MINUTES, default 15

Behind a reverse proxy, set trusted proxies

Rate limiting keys on the client IP. If your server sits behind a proxy and HINATA_TRUSTED_PROXIES is not set to the proxy's CIDR, every request appears to come from the proxy and shares one bucket. See Reverse proxy & TLS.

Exploring the full surface

The complete, always current endpoint list is served by an interactive Scalar API docs UI, gated by the HINATA_DOCS_ENABLED flag. It is off by default in production, so your API surface is not published to everyone. While developing, it is the best way to browse every route, schema and parameter.

Enable it locally before you start the server:

HINATA_DOCS_ENABLED=true ./gradlew bootRun

Or in a .env / compose environment:

HINATA_DOCS_ENABLED=true

Then open the docs UI in your browser at your server's base URL.

Do not expose the docs UI in production

The Scalar UI describes every endpoint and schema. Leave HINATA_DOCS_ENABLED=false on internet-facing deployments. Enable it only on trusted, local dev servers.

Where to go next