Docs / API

API Documentation

JSON API for bot integrations and external automation.

Klarhet JSON API

Machine-facing HTTP API under /api/v1/*. Intended for bot integrations and external automation.

Authentication

All /api/v1/* endpoints require an API key via bearer token:

Authorization: Bearer klh_<prefix>_<secret>
  • Session cookies are not accepted on /api/v1/*.
  • Tokens are bound to a user (bot or human). Permissions live on the user, not the token. To split permission sets, create multiple bot users.
  • Bot accounts inherit @mention resolution, project membership, and role checks from the existing user system.

Create tokens from the bot management page in the web UI. The plaintext secret is shown exactly once at creation time — the server only stores its hash.

Token format

klh_<8 hex prefix>_<32 base64url secret>

The prefix is a unique lookup key. The secret is hashed and verified with constant-time compare on every request.

Errors

Errors return JSON:

{"error": "not found"}

Status codes: 400 malformed body, 401 missing / invalid / disabled token, 403 permission / throttle, 404 not found or not visible, 500 server error.

Accounts that trigger the write-abuse threshold are automatically suspended pending admin review. This applies equally to API and web UI users.


Identity

GET /api/v1/me

Returns the user bound to the token. Useful for smoke-testing credentials.

{
  "id": "a3Bf9kLm2xQz",
  "login_name": "releasebot",
  "display_name": "Release Bot",
  "is_bot": true,
  "permissions": 0
}

Projects

GET /api/v1/projects

Lists projects the bound user can see.

{
  "projects": [
    {
      "id": "Xk9mP2nR4vLw",
      "name": "klarhet",
      "description": "…",
      "visibility": "public",
      "created_at": "2026-01-12T10:03:00Z"
    }
  ]
}

GET /api/v1/projects/{id}

Returns a single project if visible. 404 otherwise. The {id} parameter is the project's public ID (e.g. Xk9mP2nR4vLw).


Tickets

GET /api/v1/projects/{id}/tickets

Lists tickets for a project. Query parameters:

Param Values Default
state open, closed all
type feature, bug all
priority urgent, normal, low all
limit 1–200 50

Response:

{
  "tickets": [
    {
      "id": "Hn3kQ8wLp5Ry",
      "project_id": "Xk9mP2nR4vLw",
      "title": "Login redirect loops",
      "description": "…",
      "type": "bug",
      "priority": "urgent",
      "state": "open",
      "created_by": "a3Bf9kLm2xQz",
      "author": "Viktor",
      "created_at": "2026-04-10T09:15:22Z"
    }
  ]
}

GET /api/v1/tickets/{id}

Returns a single ticket if visible. Closed tickets also include closed_at and close_reason.

POST /api/v1/projects/{id}/tickets

Creates a ticket. Requires contributor role or higher on the project.

Request:

{
  "title": "Login redirect loops",
  "description": "Happens on Safari after OAuth.",
  "type": "bug",
  "priority": "normal"
}
  • title is required.
  • type defaults to feature if omitted or unrecognized.
  • priority defaults to normal.

Response: 201 Created with the full ticket object. Subscribed webhooks fire for this event.


Comments

GET /api/v1/tickets/{id}/comments

Lists non-deleted comments on a ticket (oldest first).

{
  "comments": [
    {
      "id": "Yt7wN1cR9mKx",
      "ticket_id": "Hn3kQ8wLp5Ry",
      "user_id": "a3Bf9kLm2xQz",
      "author": "Viktor",
      "body": "Repro steps: …",
      "created_at": "2026-04-10T09:20:00Z"
    }
  ]
}

POST /api/v1/tickets/{id}/comments

Adds a comment. Requires visibility on the ticket's project (members, or any authenticated user on public projects).

Request:

{"body": "Looks like a cookie mismatch."}

Response: 201 Created with the full comment object. Subscribed webhooks fire for this event.


Ticket relations

Directed links between tickets (blocks / duplicates / relates_to / parent_of). The relates_to type is symmetric — creating A→B with relates_to is indistinguishable from B→A.

GET /api/v1/tickets/{id}/relations

Lists every relation touching the ticket (either side).

{
  "relations": [
    {
      "id": "Qm4jT6vBn8Fw",
      "type": "blocks",
      "outgoing": true,
      "other_ticket_id": "Lp2sW7dK0xRv",
      "other_title": "Migrate auth",
      "other_state": "open",
      "created_by": "a3Bf9kLm2xQz",
      "created_at": "2026-04-15T11:02:00Z"
    }
  ]
}

outgoing = true means this ticket is the from side (e.g. "Blocks"). false means it's the to side (e.g. "Blocked by"). Symmetric relations always return outgoing: true.

POST /api/v1/tickets/{id}/relations

Creates a relation. Requires contributor role or higher on the project. Both tickets must live in the same project.

Request:

{"type": "blocks", "target_id": "Lp2sW7dK0xRv"}

Valid type values: blocks, duplicates, relates_to, parent_of.

Response: 201 Created with the new relation object. Subscribed webhooks fire for this event.

DELETE /api/v1/tickets/{id}/relations/{relID}

Removes a relation. The relation must touch the named ticket. Returns 204 No Content. Subscribed webhooks fire for this event.


Notifications

Mentions (@login in tickets/comments) create per-user notification rows. Bots can poll these instead of (or alongside) webhook delivery.

GET /api/v1/notifications

Lists the bound user's notifications, newest first. Query parameters:

Param Values Default
unread 1 for unread only all
limit 1–200 50

Response:

{
  "notifications": [
    {
      "id": "Vn8cJ3qW5tRm",
      "type": "mention",
      "title": "Viktor mentioned you in Login redirect loops",
      "link": "/projects/Xk9mP2nR4vLw/tickets/Hn3kQ8wLp5Ry#comment-Yt7wN1cR9mKx",
      "read": false,
      "created_at": "2026-04-10T09:20:00Z"
    }
  ],
  "unread_count": 1
}

The link field is a relative browser URL. Bots can parse it to recover the relevant project, ticket, and comment IDs.

POST /api/v1/notifications/{id}/read

Marks a single notification read. 404 if it isn't owned by the bound user.

POST /api/v1/notifications/read

Marks every unread notification for the bound user as read.


Webhooks

Per-project outbound HTTP hooks. Configured in the project settings page or via this API. Requires project admin role.

GET /api/v1/projects/{id}/webhooks

Lists webhooks for a project. Secrets are never returned here.

{
  "webhooks": [
    {
      "id": "Gw5rK1nM7pTx",
      "project_id": "Xk9mP2nR4vLw",
      "url": "https://example.com/hook",
      "topics": "ticket.created,comment.added",
      "status": "active",
      "created_by": "a3Bf9kLm2xQz",
      "created_at": "2026-04-01T12:00:00Z"
    }
  ]
}

POST /api/v1/projects/{id}/webhooks

Creates a webhook. The generated secret is returned only in this response; store it now or rotate.

Request:

{
  "url": "https://example.com/hook",
  "topics": "ticket.created,comment.added"
}
  • url is required.
  • topics is a comma-separated subscription filter. Empty string = all supported topics.

Response: 201 Created

{
  "id": "Gw5rK1nM7pTx",
  "project_id": "Xk9mP2nR4vLw",
  "url": "https://example.com/hook",
  "topics": "ticket.created,comment.added",
  "status": "active",
  "created_by": "a3Bf9kLm2xQz",
  "created_at": "2026-04-01T12:00:00Z",
  "secret": "<base64url, shown once>"
}

DELETE /api/v1/projects/{id}/webhooks/{hookID}

Removes a webhook. Returns 204 No Content. Any in-flight deliveries for the removed hook are abandoned.


Webhook delivery protocol

When a subscribed event fires, a delivery is enqueued for each matching active webhook.

Request

  • Method: POST
  • Content-Type: application/json
  • X-Klarhet-Topic: <topic>
  • X-Klarhet-Signature: sha256=<hex> — HMAC-SHA256 of the raw body using the webhook's secret

Body (envelope):

{
  "topic": "comment.added",
  "delivered_at": "2026-04-10T09:20:00Z",
  "data": {
    "comment_id": "Yt7wN1cR9mKx",
    "ticket_id": "Hn3kQ8wLp5Ry",
    "project_id": "Xk9mP2nR4vLw",
    "user_id": "a3Bf9kLm2xQz"
  }
}

Topics

Topic Payload fields
ticket.created ticket_id, project_id, created_by
ticket.updated ticket_id, project_id, …
ticket.closed ticket_id, project_id, …
ticket.reopened ticket_id, project_id, …
comment.added comment_id, ticket_id, project_id, user_id
ticket.relation_added relation_id, ticket_id, other_ticket_id, project_id, type, user_id
ticket.relation_removed relation_id, ticket_id, other_ticket_id, project_id, type, user_id

Receivers should re-fetch via the JSON API for the full resource — payloads carry only IDs.

Retries

Deliveries are attempted with a 10-second HTTP timeout.

  • Success: any 2xx status → delivered.
  • Failure: non-2xx or transport error → failed, next attempt scheduled per backoff.
  • Backoff (attempt → wait): 1 → 30s, 2 → 2m, 3 → 10m, 4 → 1h, 5 → 6h.
  • After 5 failed attempts → dead. No further retries.
  • Delivered records are purged after 14 days.

Verifying signatures

Pseudocode:

expected = "sha256=" + hex(hmac_sha256(body_raw, hook_secret))
constant_time_compare(expected, request.headers["X-Klarhet-Signature"])

Use the raw request body (no re-serialization) for the HMAC input.


Rate limits

There is no per-token rate limit. Write operations are throttled — accounts that exceed the abuse threshold are automatically suspended pending admin review. This applies equally to API and web UI users. Reads are unthrottled.