LabHit Docs

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/json
  • Authorization: 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: push
  • X-Hub-Signature-256: sha256=<hex> (if webhook secret is configured)

GitLab headers:

  • X-GitLab-Event: Push Hook
  • X-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-Limit
  • X-RateLimit-Remaining
  • Retry-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.