API Reference
LabHit exposes a GraphQL API for pipeline management, authentication,
extension registry, and usage tracking. The API server is started with
labhit serve.
Base URL
- Production:
https://api.labhit.dev - Local development:
http://127.0.0.1:8080
Authentication
The API supports two authentication methods:
JWT Tokens (primary)
Obtained via GitHub OAuth. Include in the Authorization
header:
Authorization: Bearer <access_token>
Access tokens expire after 15 minutes. Use the refresh endpoint to obtain a new one.
API Keys
For programmatic access (CI/CD pipelines, scripts). Include in the
Authorization header:
Authorization: Bearer <api_key>
Set via the LABHIT_API_KEY environment variable on the
server.
REST Endpoints
Health Check
GET /health
Response: "ok" (plaintext, 200)
No authentication required.
Server Info
GET /
Response:
{
"name": "LabHit Engine",
"version": "0.6.0",
"endpoints": {
"graphql": "/graphql",
"health": "/health",
"webhook": "/webhook"
}
}
GraphQL
POST /graphql
Headers:
Content-Type: application/jsonAuthorization: Bearer <token>(optional, required for mutations)
Request body:
{
"query": "{ health }",
"variables": {}
}
See GraphQL Schema below for available queries and mutations.
GraphQL Playground
GET /graphql/playground
Interactive GraphQL IDE. Only available when the server is started
with --playground.
Authentication Endpoints
Authentication requires JWT_SECRET,
GITHUB_CLIENT_ID, and GITHUB_CLIENT_SECRET
environment variables.
Device Flow: Request Code (CLI)
POST /auth/device
Request body: {}
Response:
{
"device_code": "ABC123...",
"user_code": "WXYZ-1234",
"verification_uri": "https://github.com/login/device",
"expires_in": 900,
"interval": 5
}
The CLI displays user_code and opens
verification_uri in the browser.
Device Flow: Poll for Token (CLI)
POST /auth/device/token
Request body:
{
"device_code": "ABC123..."
}
Response (authorized):
{
"status": "authorized",
"access_token": "eyJ0eXAi...",
"refresh_token": "eyJ0eXAi...",
"user": {
"id": "...",
"email": "user@example.com",
"name": "Jane Doe",
"github_login": "janedoe",
"avatar_url": "https://...",
"tier": "free"
}
}
Response (pending):
{
"status": "pending",
"error": "authorization_pending"
}
Poll at the interval specified in the device code
response until status is "authorized" or the
code expires.
Web OAuth: Login Redirect
GET /auth/web/login
Redirects the browser to GitHub's OAuth consent page. Used by the web dashboard.
Web OAuth: Callback
GET /auth/callback/web?code=<code>&state=<state>
Exchanges the GitHub authorization code for tokens and redirects to the dashboard with tokens in the URL fragment:
https://app.labhit.dev/auth/callback#access_token=...&refresh_token=...
Refresh Token
POST /auth/refresh
Request body:
{
"refresh_token": "eyJ0eXAi..."
}
Response:
{
"access_token": "eyJ0eXAi...",
"user": {
"id": "...",
"email": "user@example.com",
"tier": "free"
}
}
Logout
POST /auth/logout
Request body:
{
"refresh_token": "eyJ0eXAi..."
}
Revokes all sessions for the user.
Current User
GET /auth/me
Headers:
Authorization: Bearer <access_token> (required)
Response:
{
"id": "...",
"email": "user@example.com",
"name": "Jane Doe",
"github_login": "janedoe",
"avatar_url": "https://...",
"tier": "free",
"is_admin": false,
"created_at": "2026-03-16T10:00:00Z"
}
SSE Log Streaming
GET /pipeline/{run_id}/logs
Server-Sent Events stream for real-time pipeline execution logs.
Authentication:
Authorization: Bearer <token> (if an API key is
configured). Browser EventSource clients cannot set request
headers, so the token may instead be supplied as a
?token=<token> query parameter on the URL.
Event types:
| Event | Description |
|---|---|
stage_started |
Stage execution began |
stage_completed |
Stage finished (success or failed) |
stage_skipped |
Stage skipped (unmet dependencies) |
pipeline_completed |
Entire pipeline finished |
pipeline_cancelled |
Pipeline was cancelled |
Example stream:
event: stage_started
data: {"type":"stage_started","stage":"build","status":"Running","timestamp":"2026-03-16T10:30:00Z"}
event: stage_completed
data: {"type":"stage_completed","stage":"build","status":"Success","logs":"...","timestamp":"2026-03-16T10:31:00Z"}
event: pipeline_completed
data: {"type":"pipeline_completed","status":"SUCCESS","timestamp":"2026-03-16T10:32:00Z"}
The server sends a keep-alive ping every 15 seconds.
Webhooks
POST /webhook
Receives push events from GitHub or GitLab.
GitHub headers:
X-GitHub-Event: pushX-Hub-Signature-256: sha256=<hex>(if webhook secret is configured)
GitLab headers:
X-GitLab-Event: Push HookX-Gitlab-Token: <secret>(if token is configured)
Response:
{
"accepted": true,
"provider": "github",
"repository": "org/repo",
"ref": "refs/heads/main",
"commit": "abc123def456",
"run_id": "019506a3-1234-7000-8000-000000000001",
"status": "Success"
}
When LABHIT_WEBHOOK_PIPELINE is set, the webhook
auto-triggers a pipeline with template variables injected. See Webhook Integration.
Configure the webhook secret via the
LABHIT_WEBHOOK_SECRET environment variable.
GraphQL Schema
Queries
health
Server health status.
{
health
}
Returns: "ok"
version
Engine version string.
{
version
}
Returns the engine workspace version string, e.g.
"0.6.0".
Server metadata
Server configuration (build mode, log level, and similar metadata) is
not part of the public API surface. Queries that return server
configuration require authentication. Use the version query
above for the public engine version.
runs
List pipeline runs.
{
runs(limit: 10) {
id
pipelineName
status
startedAt
finishedAt
stages {
name
status
}
}
}
run
Get a specific pipeline run by ID.
{
run(id: "019...") {
id
pipelineName
status
stages {
name
status
startedAt
finishedAt
outputs {
key
value
}
}
}
}
pipelineHealth
Pipeline health computed from recent run history: success rate,
flaky-stage detection (stages that both pass and fail), slowest stages,
and the count of runs reconciled to interrupted by crash
recovery. Scoped to the caller's runs.
{
pipelineHealth(pipelineName: "ci", limit: 50) {
pipelineName
totalRuns
succeeded
failed
interrupted
successRate
flakyStages
stages {
name
runs
failures
avgDurationSecs
flaky
}
}
}
A stage is flagged flaky when it both succeeded and
failed within the window. successRate is
succeeded / terminal runs (still-running runs are excluded;
interrupted runs count against it). See Reliability & Diagnostics for how
interrupted runs arise.
me
Current authenticated user profile (requires JWT).
{
me {
id
email
tier
isAdmin
pipelineRunLimit
maxConcurrent
canUsePrivateExtensions
canPublishExtensions
}
}
usage
Usage metrics for the authenticated user (requires JWT).
{
usage {
metric
total
limit
percentage
status
periodStart
periodEnd
}
}
extensions
Search the extension registry.
{
extensions(query: "scanner", category: "scan", limit: 10) {
id
name
description
category
downloadCount
createdAt
}
}
extension
Get a single extension by ID.
{
extension(id: "scan/trivy") {
id
name
description
category
downloadCount
}
}
extensionVersions
List versions for an extension.
{
extensionVersions(extension_id: "scan/trivy") {
version
packageHash
sizeBytes
yanked
publishedAt
}
}
artifacts
Get artifacts from a pipeline run.
{
artifacts(run_id: "019...") {
stage
key
value
}
}
Mutations
triggerPipeline
Execute a pipeline from YAML.
mutation {
triggerPipeline(yaml: "engine: \"1\"\npipeline:\n name: test\nstages:\n greet:\n run: echo hello") {
run {
id
status
}
summary {
passed
failed
skipped
}
}
}
Requires authentication (JWT or API key).
cancelPipeline
Cancel a running or pending pipeline.
mutation {
cancelPipeline(id: "019...") {
run {
id
status
}
cancelledStages
}
}
Only pipelines in Pending, Queued, or Running state can be cancelled. Requires authentication (JWT or API key).
createApiToken
Create a personal API token for programmatic access (requires JWT).
mutation {
createApiToken(name: "CI Token", scopes: ["trigger", "read"]) {
id
name
token
scopes
createdAt
}
}
The token field contains the plaintext token, prefixed
with lh_. It is only returned once, so store it
securely.
revokeApiToken
Delete a personal API token (requires JWT).
mutation {
revokeApiToken(id: "019...")
}
Returns true if the token was deleted, error if not
found or not owned by you.
apiTokens (Query)
List your API tokens (requires JWT).
{
apiTokens {
id
name
scopes
createdAt
lastUsedAt
}
}
Token hashes are never returned, only metadata.
Types
type PipelineRun {
id: String!
pipelineName: String!
status: RunStatus!
stages: [StageRun!]!
startedAt: String!
finishedAt: String
}
type StageRun {
name: String!
status: RunStatus!
startedAt: String
finishedAt: String
outputs: [KeyValue!]!
}
type KeyValue {
key: String!
value: String!
}
enum RunStatus {
PENDING
QUEUED
RUNNING
SUCCESS
FAILED
SKIPPED
CANCELLED
}
type TriggerResult {
run: PipelineRun!
summary: RunSummary!
}
type RunSummary {
passed: Int!
failed: Int!
skipped: Int!
}
type UserProfile {
id: String!
email: String!
tier: String!
isAdmin: Boolean!
pipelineRunLimit: Int!
maxConcurrent: Int!
canUsePrivateExtensions: Boolean!
canPublishExtensions: Boolean!
}
type UsageSummary {
metric: String!
total: Int!
limit: Int!
percentage: Float!
status: String!
periodStart: String!
periodEnd: String!
}
type Artifact {
stage: String!
key: String!
value: String!
}
type RegistryExtension {
id: String!
name: String!
description: String!
category: String!
downloadCount: String!
createdAt: String!
updatedAt: String!
}
type ExtensionVersion {
extensionId: String!
version: String!
packageHash: String!
sizeBytes: String!
readme: String
changelog: String
yanked: Boolean!
publishedAt: String!
}
type CancelResult {
run: PipelineRun!
cancelledStages: Int!
}
type ApiTokenInfo {
id: String!
name: String!
scopes: [String!]!
createdAt: String!
lastUsedAt: String
}
type CreateApiTokenResult {
id: String!
name: String!
token: String!
scopes: [String!]!
createdAt: String!
}
Rate Limits
Rate limits are applied per client IP and tiered by request intent, not by HTTP method. Each tier has its own independent per-minute budget.
| Tier | Default limit | Applies to |
|---|---|---|
| Read | ~240 requests/minute per IP | Read-only requests (e.g. health, server info) |
| API | ~240 requests/minute per IP | GraphQL operations (POST /graphql) and authenticated
writes |
| Webhook | ~600 requests/minute per IP | Inbound webhook deliveries (POST /webhook) |
Each tier is independently tunable via an environment variable on the server:
| Variable | Tier |
|---|---|
LABHIT_RATE_LIMIT_READ |
Read |
LABHIT_RATE_LIMIT_API |
API |
LABHIT_RATE_LIMIT_WEBHOOK |
Webhook |
Response headers on rate-limited requests:
X-RateLimit-LimitX-RateLimit-RemainingRetry-After
Security Headers
All responses include:
| Header | Value |
|---|---|
X-Content-Type-Options |
nosniff |
X-Frame-Options |
DENY |
X-XSS-Protection |
1; mode=block |
Referrer-Policy |
strict-origin-when-cross-origin |
Content-Security-Policy |
default-src 'none'; frame-ancestors 'none' |
Strict-Transport-Security |
max-age=31536000; includeSubDomains |
Error Responses
GraphQL errors
{
"data": null,
"errors": [
{
"message": "unauthorized: valid access token required",
"extensions": { "code": "UNAUTHENTICATED" }
}
]
}
HTTP status codes
| Code | Meaning |
|---|---|
200 |
Success |
400 |
Invalid request (malformed JSON, missing fields) |
401 |
Missing or invalid authentication |
404 |
Resource not found |
405 |
Method not allowed |
429 |
Rate limit exceeded |
500 |
Server error |
CORS
When the server is started with --cors-origins,
cross-origin requests are supported:
labhit serve --cors-origins https://app.labhit.dev,http://localhost:3000
Preflight responses are cached for 1 hour. Allowed methods: GET, POST, OPTIONS. Allowed headers: Content-Type, Authorization.