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.4.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.
Headers:
Authorization: Bearer <token> (if API key is
configured)
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 |
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 version string, e.g. "0.4.0".
engineConfig
Server configuration summary.
{
engineConfig {
version
mode
logLevel
apiAuthenticated
}
}
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
}
}
}
}
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 — 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 EngineConfig {
version: String!
mode: String!
logLevel: String!
apiAuthenticated: Boolean!
}
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
| Operation | Limit |
|---|---|
| Read endpoints (GET) | 100 requests/minute per IP |
| Write endpoints (POST) | 10 requests/minute per IP |
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.