Docs / API

API Reference

Build integrations and automate your workflow with the Klarhet JSON API.

All endpoints live under /api/v1/. Responses are JSON. All requests require authentication.


Authentication

All endpoints require an API key passed as a bearer token:

Authorization: Bearer klh_<prefix>_<secret>

Create API keys from the bot management page in the admin panel. The key is shown once at creation — store it somewhere safe.

Tokens are tied to a user account (bot or human). The user's roles and project memberships determine what the token can access. To separate permission sets, create multiple bot accounts.


Errors

Errors return JSON:

{"error": "not found"}
Status Meaning
400 Malformed request body
401 Missing or invalid token
403 Insufficient permissions
404 Not found or not visible
500 Server error

Identity

GET /api/v1/me

Returns the user tied to the token.

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

Projects

GET /api/v1/projects

Lists projects visible to the authenticated user.

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

GET /api/v1/projects/{id}

Returns a single project. 404 if not visible.


Tickets

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

Lists tickets in a project.

Param Values Default
state open, closed all
type feature, bug all
priority p1, p2, p3 all
search text
limit 1–200 50

Response:

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

assigned_to and assignee are only present when someone is assigned. Closed tickets also include closed_at and close_reason.

GET /api/v1/tickets/{id}

Returns a single ticket. Includes labels when the ticket has any:

{
  "id": "Hn3kQ8wLp5Ry",
  "labels": [
    {"id": "Km8nT4wQ2xRv", "name": "backend", "color": "#6366f1"}
  ]
}

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

Creates a ticket. Requires contributor role or higher.

{
  "title": "Login redirect loops",
  "description": "Happens on Safari after OAuth.",
  "type": "bug",
  "priority": "p2"
}
  • title is required.
  • type defaults to feature.
  • priority defaults to p2 (normal). Values: p1 (urgent), p2 (normal), p3 (low).

Returns 201 with the full ticket object.

PATCH /api/v1/tickets/{id}

Updates a ticket. All fields are optional — only include what you want to change.

{
  "title": "Updated title",
  "description": "New description",
  "type": "bug",
  "priority": "p1",
  "status": "in-progress",
  "assigned_to": "b4Cg0lNn3yRa"
}
Field Values
type feature, bug
priority p1, p2, p3
status todo, in-progress, review, done
assigned_to User ID, or "" to unassign

Returns 200 with the updated ticket.

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

Closes a ticket.

{"close_reason": "fixed"}

close_reason is optional. Values: fixed (default), wontfix, notapplicable.

Returns 200 with the updated ticket.

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

Reopens a closed ticket. No request body needed.

Returns 200 with the updated ticket.


Comments

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

Lists 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.

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

Returns 201 with the comment.


Ticket relations

Links between tickets: blocks, duplicates, relates_to, parent_of.

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

Lists all relations touching this ticket.

{
  "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 source (e.g. "blocks"). false means it's the target (e.g. "blocked by").

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

Creates a relation. Both tickets must be in the same project.

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

Valid types: blocks, duplicates, relates_to, parent_of.

Returns 201 with the relation.

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

Removes a relation. Returns 204.


Notifications

Mentions (@user in tickets and comments) and watcher events create notifications.

GET /api/v1/notifications

Lists notifications for the authenticated user, newest first.

Param Values Default
unread 1 all
limit 1–200 50
{
  "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
}

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

Marks a single notification as read.

POST /api/v1/notifications/read

Marks all notifications as read.


Webhooks

Per-project outbound HTTP hooks. Requires project admin role.

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

Lists webhooks for a project. Secrets are never returned.

{
  "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 secret is returned only in this response — store it immediately.

{
  "url": "https://example.com/hook",
  "topics": "ticket.created,comment.added"
}
  • url is required.
  • topics is a comma-separated filter. Empty = subscribe to everything.

Returns 201. The response includes a secret field.

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

Removes a webhook. Returns 204.


Webhook delivery

When a matching event fires, Klarhet sends a POST to each subscribed webhook.

Headers

  • Content-Type: application/json
  • X-Klarhet-Topic — the event topic
  • X-Klarhet-Signature — HMAC-SHA256 signature of the request body, formatted as sha256=<hex>

Payload

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

Payloads contain IDs only. Fetch the full resource via the API if you need more detail.

Topics

Topic Data 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

Verifying signatures

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

Use the raw request body for the HMAC input — don't re-serialize.

Retries

Deliveries time out after 10 seconds. On failure, retries follow this schedule:

Attempt Wait
1 30 seconds
2 2 minutes
3 10 minutes
4 1 hour
5 6 hours

After 5 failures the delivery is marked dead. Delivered records are cleaned up after 14 days.