← Zurück

Public API v1

Tenant-scoped REST-API für externe Tools (z. B. Claude Code, CLIs, Scripts), um Einträge auf Boards zu lesen, anzulegen und zu aktualisieren. Dieselben Schlüssel bedienen auch den MCP-Server, mit dem KI-Assistenten direkt mit deinem Board sprechen.

Base URL

https://roadlight.pro/api/v1

Lokal:

http://localhost:3000/api/v1

Authentifizierung

Jeder Request braucht einen API-Schlüssel im Authorization-Header:

Authorization: Bearer vt_live_<token>

API-Schlüssel werden im Tenant-Admin-UI unter „API-Schlüssel“ erstellt (/tenant-admin.html?tenant=<slug>). Der Klartext-Token wird nur einmal beim Erstellen angezeigt – danach speichert die Datenbank nur einen SHA-256-Hash.

Jeder Schlüssel ist auf genau einen Tenant gescoped – er sieht ausschließlich Boards und Einträge dieses Workspaces.

Scopes

ScopeErlaubt
suggestions:readBoards und Einträge lesen
suggestions:writeEinträge anlegen (auto-freigegeben)
suggestions:statusStatus, Priorität und Labels ändern
comments:readKommentare lesen
comments:writeAdmin-Kommentare schreiben (auto-freigegeben)
boards:writeBoards anlegen und umbenennen
releases:readReleases lesen
releases:writeReleases anlegen, ändern, löschen und Einträge zuordnen

Fehlt ein erforderlicher Scope, gibt der Endpoint 403 zurück. Bestehende Schlüssel bekommen durch neue Scopes nichts dazu – sie müssen an einem Schlüssel explizit gesetzt sein.

Für einen kompletten Workspace-Aufbau per API (Board anlegen, Einträge importieren, Releases nachbauen und zuordnen) braucht der Schlüssel: boards:write, suggestions:read, suggestions:write, suggestions:status, releases:read, releases:write.

Rate Limits

Pro Schlüssel:

Überschreitung → 429 Too Many Requests.

Fehler-Format

{ "error": "human-readable message" }

Endpoints: Einträge & Kommentare

GET /me

Sanity-Check für den Schlüssel. Gibt Tenant- und Scope-Info zurück.

curl -H "Authorization: Bearer vt_live_…" \
  https://roadlight.pro/api/v1/me

Response:

{
  "tenant": { "id": "tenant_abc", "slug": "acme", "name": "Acme Workspace" },
  "key": { "name": "Claude Code lokal", "scopes": ["suggestions:read", "suggestions:write"] }
}

GET /apps

Liste der Boards im Tenant. Benötigt suggestions:read.

curl -H "Authorization: Bearer vt_live_…" \
  https://roadlight.pro/api/v1/apps

Response:

[
  {
    "id": "app_xyz",
    "slug": "customer-feedback",
    "name": "Customer Feedback",
    "description": "Wünsche und Bugs unserer Kund:innen",
    "ticketPrefix": "CF"
  }
]

GET /apps/:appSlug/suggestions

Liste aller Einträge in einem Board (auch nicht-freigegebene). Benötigt suggestions:read.

Query-Filter (optional):

curl -H "Authorization: Bearer vt_live_…" \
  "https://roadlight.pro/api/v1/apps/customer-feedback/suggestions?type=bug&status=offen"

POST /apps/:appSlug/suggestions

Neuen Eintrag erstellen – automatisch freigegeben. Benötigt suggestions:write.

Body:

{
  "type": "feature",
  "title": "Dark mode für die Mobile App",
  "description": "Auf dem iPhone fehlt aktuell der Dark-Mode-Schalter."
}

Bug-Variante:

{
  "type": "bug",
  "title": "Login schlägt mit 500 fehl",
  "description": "Beim Login mit Google kommt 500.",
  "severity": "high",
  "stepsToReproduce": "1. /login öffnen\n2. Google-Button klicken",
  "expectedBehavior": "Erfolgreicher Login",
  "actualBehavior": "500 Internal Server Error",
  "environment": { "platform": "iOS 17.4", "browser": "Safari 17" }
}

Ticket-Variante:

{
  "type": "ticket",
  "title": "Domain umziehen",
  "description": "DNS-Records auf Cloudflare migrieren",
  "priority": "hoch"
}

Response (201):

{
  "id": "sug_abc123",
  "ticketNumber": "CF-042",
  "type": "feature",
  "title": "Dark mode für die Mobile App",
  "description": "…",
  "status": "neu",
  "priority": "mittel",
  "labels": [],
  "approved": true,
  "votes": 0,
  "appId": "app_xyz",
  "tenantId": "tenant_abc",
  "severity": null,
  "createdAt": "2026-05-21T14:12:33.000Z"
}

Import bestehender Einträge

Beim Übertragen eines gewachsenen Boards vergibt der Server sonst Ticketnummer und Datum selbst. Der optionale import-Block übernimmt stattdessen die Werte aus der Quelle. Er hängt an suggestions:write und greift nur, wenn er explizit mitgeschickt wird – normale Einreichungen ändern sich nicht.

{
  "type": "feature",
  "title": "Einkaufsliste teilen",
  "description": "…",
  "import": {
    "ticketNumber": "FAM-041",
    "votes": 2,
    "createdAt": "2026-04-30T09:12:00.000Z"
  }
}
FeldVerhalten
ticketNumber Überschreibt den Generator. Muss zum Ticket-Prefix des Boards passen und im Board eindeutig sein (Kollision → 409). Hebt den Board-Zähler an: nach einem Import von FAM-140 bekommt der nächste regulär angelegte Eintrag FAM-141.
votes Setzt den Stimmen-Zähler (ganze Zahl ≥ 0). Es werden keine votes-Dokumente erzeugt, damit die Doppelabstimmungs-Sperre sauber bleibt.
createdAt Überschreibt den Serverzeitstempel. Nur Vergangenheit; ungültig oder in der Zukunft → 400.

Alle Felder sind einzeln optional, mindestens eines muss gesetzt sein.

Import-Schreibvorgänge landen im Audit-Log mit action: "imported" und actor: "api:<keyId>" und sind so von regulären Anlagen unterscheidbar.

GET /suggestions/:id

Einzelnen Eintrag laden. Benötigt suggestions:read. 404, wenn Eintrag nicht zum Tenant des Schlüssels gehört.

PATCH /suggestions/:id

Status, Priorität und/oder Labels ändern. Benötigt suggestions:status.

Body (alle Felder optional, mindestens eines erforderlich):

{
  "status": "wird umgesetzt",
  "priority": "hoch",
  "labels": ["mobile", "ux"]
}

Setzt automatisch approved: true, falls ein Status ≠ neu gesetzt wird.

GET /suggestions/:id/comments

Liste aller Kommentare (auch pending). Benötigt comments:read.

POST /suggestions/:id/comments

Admin-Kommentar hinzufügen (auto-freigegeben). Benötigt comments:write.

{
  "text": "Wir haben das in Sprint 42 eingeplant.",
  "screenshots": []
}

screenshots ist optional und akzeptiert Base64-Data-URLs (data:image/png;base64,…), max. 5 Bilder, je max. 300 KB, gesamt max. 800 KB.

Boards

POST /apps

Board anlegen. Benötigt boards:write. Der Tenant kommt aus dem Schlüssel – es gibt keinen Tenant-Parameter.

{
  "name": "FamilyManager",
  "slug": "familymanager",
  "ticketPrefix": "FAM",
  "description": "Wünsche und Bugs aus der Familie"
}

Response (201):

{
  "id": "app_xyz",
  "slug": "familymanager",
  "name": "FamilyManager",
  "description": "Wünsche und Bugs aus der Familie",
  "ticketPrefix": "FAM"
}

PATCH /apps/:appSlug

Board umbenennen bzw. Beschreibung ändern. Benötigt boards:write.

{ "name": "Family Manager", "description": "Neuer Text" }

slug und ticketPrefix sind unveränderlich – am Slug hängen bestehende Key-Integrationen und öffentliche Board-URLs, am Prefix die bereits vergebenen Ticketnummern. Werden sie mitgeschickt, antwortet der Endpoint mit 400.

Releases

GET /apps/:appSlug/releases

Releases eines Boards inklusive der zugeordneten Einträge (items[], nur freigegebene). Benötigt releases:read.

Query-Filter (optional): status – kommagetrennt, aus geplant, in Arbeit, veröffentlicht.

curl -H "Authorization: Bearer vt_live_…" \
  "https://roadlight.pro/api/v1/apps/familymanager/releases?status=geplant"

Response:

[
  {
    "id": "rel_abc",
    "appId": "app_xyz",
    "version": "2.3.0",
    "title": "Kalender-Sync",
    "description": "…",
    "status": "geplant",
    "releaseDate": "2026-10-01T00:00:00.000Z",
    "items": [
      {
        "id": "sug_1",
        "ticketNumber": "FAM-041",
        "title": "Einkaufsliste teilen",
        "type": "feature",
        "status": "wird umgesetzt"
      }
    ]
  }
]

POST /apps/:appSlug/releases

Release anlegen. Benötigt releases:write. Das Board kommt aus dem Pfad – eine appId im Body wird ignoriert.

{
  "version": "2.3.0",
  "title": "Kalender-Sync",
  "description": "Was in diesem Release steckt",
  "status": "geplant",
  "releaseDate": "2026-10-01"
}

PATCH /releases/:releaseId

Teilfelder eines Releases ändern (version, title, description, status, releaseDate). Benötigt releases:write. Ein Release aus einem fremden Workspace ist 404.

{ "status": "veröffentlicht", "releaseDate": null }

"releaseDate": null löscht das Datum (nicht: ignoriert es). Response ist das aktualisierte Release.

DELETE /releases/:releaseId

Release löschen. Benötigt releases:write. Zugeordnete Einträge werden nur entkoppelt, nicht gelöscht.

{ "success": true, "message": "Release gelöscht", "unlinkedSuggestions": 12 }

PUT /suggestions/:id/release

Eintrag einem Release zuordnen. Benötigt releases:write. null hebt die Zuordnung auf.

{ "releaseId": "rel_abc" }

Konstanten

Suggestion-Typen: feature, bug, ticket

Status (Feature): neu, wird geprüft, wird umgesetzt, im Test, ist umgesetzt, wird nicht umgesetzt

Status (Bug/Ticket): neu, offen, in Bearbeitung, im Test, wartend, gelöst, geschlossen

Prioritäten: niedrig, mittel, hoch, kritisch

Bug-Severities: low, medium, high, critical (wird beim Erstellen automatisch in priority gemappt)

Audit

Jeder eintragsbezogene Write-Call (POST/PATCH/PUT) wird mit actor: "api:<keyId>" protokolliert; Importe bekommen dabei action: "imported" statt "created". Im Tenant-Admin-UI sind die Aktionen in der Eintrags-Historie sichtbar. Board- und Release-Anlagen hängen an keiner Ticket-ID und erscheinen daher nicht in der Eintrags-Historie.

Beispiel: Eintrag aus einem Bash-Script anlegen

#!/usr/bin/env bash
set -euo pipefail

: "${VOTING_TOOL_API_KEY:?Bitte API-Schlüssel in VOTING_TOOL_API_KEY setzen}"

curl -fsS -X POST \
  -H "Authorization: Bearer $VOTING_TOOL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "bug",
    "title": "CI rot nach Merge",
    "description": "PR #42 hat E2E-Tests gerötet",
    "severity": "medium",
    "stepsToReproduce": "main pullen, npm test",
    "expectedBehavior": "alle Tests grün",
    "actualBehavior": "tenant-admin.spec.js schlägt fehl"
  }' \
  https://roadlight.pro/api/v1/apps/customer-feedback/suggestions