Skip to main content

Webhook API

Complete API reference for Flux-Orbit's webhook endpoints and deployment triggers.

Base URL

http://YOUR_SERVER:9001

Default port is 9001, configurable via WEBHOOK_PORT environment variable.

Available Endpoints

MethodEndpointDescriptionAuthentication
POST/webhookTrigger a deploymentWebhook signature (optional)
GET/statusGet current deployment status and progressAPI key (if configured)
GET/healthCheck application and deployment healthPublic
GET/logs/:idGet build logs for specific releaseAPI key (if configured)

Authentication

API Key Authentication

The /status and /logs/:id endpoints support optional API key authentication via the API_KEY environment variable. The /health endpoint is always public (no authentication required).

When API_KEY is set:

# Protect API endpoints
docker run -d -e API_KEY=your_secret_key_here ...

# Access with authentication
curl -H "Authorization: Bearer your_secret_key_here" http://localhost:9001/status
curl -H "Authorization: Bearer your_secret_key_here" http://localhost:9001/logs/1

When API_KEY is NOT set:

# Endpoints are publicly accessible (default)
curl http://localhost:9001/status # No authentication required
curl http://localhost:9001/logs/1 # No authentication required

Authentication Methods:

The API accepts the key in the Authorization header using either format:

  • Authorization: Bearer <api_key>
  • Authorization: Token <api_key>

Example Responses:

Unauthorized (401):

{
"error": "Unauthorized",
"message": "Valid API key required"
}
Security Note

The API_KEY is independent from WEBHOOK_SECRET:

  • WEBHOOK_SECRET - Used for webhook signature verification (POST /webhook)
  • API_KEY - Used for API endpoint authentication (GET /status, GET /logs/:id)

This separation allows you to share webhook secrets with GitHub/GitLab without exposing API access. The GET /health endpoint is always public for monitoring purposes.

Endpoints

POST /webhook

Trigger a deployment via webhook.

Request

Headers:

Content-Type: application/json
X-GitHub-Event: push (optional)
X-Hub-Signature-256: sha256=... (optional, for verification)

Body:

{
"ref": "refs/heads/main",
"repository": {
"clone_url": "https://github.com/user/repo.git"
},
"after": "commit_sha_here",
"pusher": {
"name": "username"
}
}

Response

Success (200 OK):

{
"status": "success",
"message": "Deployment queued",
"delivery_id": "unique-id-here"
}

Error (400 Bad Request):

{
"status": "error",
"message": "Invalid payload"
}

Signature Verification Failed (401 Unauthorized):

{
"status": "error",
"message": "Webhook signature verification failed"
}

GET /status

Get current deployment status.

Response

Idle State (No Deployment Running):

{
"status": "idle",
"current_release": "001-abc123def456789",
"last_deployment": {
"commit": "abc123d",
"commit_full": "abc123def456789",
"updated_at": "2024-01-01T12:00:00+00:00",
"build_duration_seconds": 45,
"build_status": "success"
},
"releases": [
{
"id": "001-abc123def456789",
"commit": "abc123def456789",
"commit_message": "Fix login bug",
"build_duration_seconds": 45,
"deployed_at": "2024-01-01T12:00:00+00:00"
}
]
}

Idle State (With Failed Deployment):

{
"status": "idle",
"current_release": "001-abc123def456789",
"last_deployment": {
"commit": "abc123d",
"commit_full": "abc123def456789",
"updated_at": "2024-01-01T12:00:00+00:00",
"build_duration_seconds": 45,
"build_status": "success"
},
"last_failed_deployment": {
"commit": "def456a",
"commit_full": "def456abc789012",
"failed_at": "2024-01-01T13:00:00.000Z",
"reason": "build_failed"
},
"failed_commits_count": 1
}

Deployment In Progress:

{
"status": "deploying",
"stage": "building",
"commit": "def456a",
"commit_full": "def456abc789012",
"rollback_commit": "abc123d",
"started_at": 1704110400,
"duration_seconds": 120,
"timeout_seconds": 1800,
"progress_percent": 6
}

Deployment Stages

The stage field can have these values during deployment:

  • starting - Deployment initiated
  • git_pull - Pulling latest code from repository
  • installing_dependencies - Installing npm/pip/bundle packages
  • stopping_app - Stopping current application
  • building - Running build commands
  • starting_app - Starting new application version
  • health_check - Verifying application health
  • completed - Deployment successful

GET /health

Check application and deployment health.

Public Endpoint

This endpoint is always publicly accessible (no API key required) to support external monitoring and health check services.

Response

Application Healthy:

{
"healthy": true,
"app_status": "running",
"app_port": 3000,
"supervisor_status": "RUNNING",
"deployment_in_progress": false,
"deployment_stage": "none"
}

Application Not Responding:

{
"healthy": false,
"app_status": "not_responding",
"app_port": 3000,
"supervisor_status": "STOPPED",
"deployment_in_progress": false,
"deployment_stage": "none"
}

During Deployment:

{
"healthy": true,
"app_status": "running",
"app_port": 3000,
"supervisor_status": "RUNNING",
"deployment_in_progress": true,
"deployment_stage": "building"
}

GET /logs/:identifier

Get build logs for a specific release. Supports flexible identifier matching.

URL Parameters

ParameterDescriptionExamples
identifierRelease identifier (multiple formats supported)1, 001, 001-abc123..., abc1234

Query Parameters

ParameterTypeDescription
tailintegerReturn only the last N lines (optional)
formatstringResponse format: text (default) or json

Supported Identifier Formats

  • Full release ID: 001-40c4e6f944123aa03dcfd1ef604e0143d97187dc
  • Short release ID: 001 (just the sequence number with leading zeros)
  • Release number: 1 (sequence number without leading zeros)
  • Full commit hash: 40c4e6f944123aa03dcfd1ef604e0143d97187dc
  • Short commit hash: 40c4e6f (first 7 characters)
  • Failed build: 40c4e6f-FAILED or just 40c4e6f (will match either successful or failed build)
Failed Build Logs

When a build fails, the log is saved as {commit}-FAILED.log. You can access it using the commit hash as the identifier. For example, if commit abc1234 failed to build, use /logs/abc1234 to view the failure log.

Response

Success (200 OK) - Text Format (default):

=== Build Metadata ===
{
"commit": "40c4e6f944123aa03dcfd1ef604e0143d97187dc",
"project_type": "node",
"serve_type": "static",
"build_duration": 45,
"build_timestamp": "2025-12-04T10:30:00+00:00",
"build_log_file": "/tmp/build_40c4e6f.log"
}

=== Build Log ===
=== Installing Dependencies ===
npm install --production
added 120 packages in 12s

=== Building Application ===
npm run build
> build
> vite build

vite v5.0.0 building for production...
✓ 45 modules transformed.
dist/index.html 0.45 kB
dist/assets/index-abc123.js 45.67 kB
✓ built in 5.23s

Success (200 OK) - JSON Format:

{
"identifier": "1",
"log_file": "001-40c4e6f944123aa03dcfd1ef604e0143d97187dc.log",
"line_count": 156,
"content": "=== Build Metadata ===\n{...}\n\n=== Build Log ===\n...",
"lines": [
"=== Build Metadata ===",
"{",
" \"commit\": \"40c4e6f944123aa03dcfd1ef604e0143d97187dc\",",
"...",
]
}

Not Found (404):

{
"error": "Not Found",
"message": "No build log found for identifier: 999",
"hint": "Use GET /status to see available releases"
}

Unauthorized (401):

{
"error": "Unauthorized",
"message": "Valid API key required"
}

Examples

# Get logs for latest release (text format)
curl http://localhost:9001/logs/1

# Get logs by release ID
curl http://localhost:9001/logs/001

# Get logs by commit hash (short)
curl http://localhost:9001/logs/40c4e6f

# Get last 50 lines
curl "http://localhost:9001/logs/1?tail=50"

# Get logs as JSON with metadata
curl "http://localhost:9001/logs/1?format=json"

# With authentication
curl -H "Authorization: Bearer your_api_key" http://localhost:9001/logs/1

Webhook Payloads by Provider

GitHub

Push Event

{
"ref": "refs/heads/main",
"before": "9049f128...",
"after": "0d1a26e6...",
"repository": {
"id": 186853002,
"name": "my-app",
"full_name": "user/my-app",
"private": false,
"owner": {
"name": "user",
"email": "user@example.com"
},
"clone_url": "https://github.com/user/my-app.git",
"default_branch": "main"
},
"pusher": {
"name": "user",
"email": "user@example.com"
},
"sender": {
"login": "user",
"id": 1234567
},
"commits": [
{
"id": "0d1a26e6...",
"message": "Update app.js",
"timestamp": "2024-01-01T00:00:00Z",
"author": {
"name": "User Name",
"email": "user@example.com"
}
}
]
}

Release Event

{
"action": "published",
"release": {
"id": 1234567,
"tag_name": "v1.0.0",
"target_commitish": "main",
"name": "Version 1.0.0",
"draft": false,
"prerelease": false,
"created_at": "2024-01-01T00:00:00Z",
"published_at": "2024-01-01T00:00:00Z"
},
"repository": {
"clone_url": "https://github.com/user/repo.git"
}
}

GitLab

Push Event

{
"object_kind": "push",
"ref": "refs/heads/main",
"checkout_sha": "da1560886...",
"before": "95790bf891...",
"after": "da1560886...",
"project": {
"id": 15,
"name": "my-app",
"web_url": "https://gitlab.com/user/my-app",
"git_http_url": "https://gitlab.com/user/my-app.git",
"git_ssh_url": "git@gitlab.com:user/my-app.git",
"default_branch": "main"
},
"commits": [
{
"id": "da1560886...",
"message": "Update app.js",
"timestamp": "2024-01-01T00:00:00+00:00",
"author": {
"name": "User Name",
"email": "user@example.com"
}
}
]
}

Bitbucket

Push Event

{
"push": {
"changes": [
{
"new": {
"type": "branch",
"name": "main",
"target": {
"hash": "abc123def456"
}
},
"old": {
"type": "branch",
"name": "main",
"target": {
"hash": "789xyz000111"
}
}
}
]
},
"repository": {
"type": "repository",
"full_name": "user/my-app",
"name": "my-app",
"links": {
"clone": [
{
"name": "https",
"href": "https://bitbucket.org/user/my-app.git"
}
]
}
}
}

Webhook Security

Signature Verification

Flux-Orbit supports signature verification for:

GitHub (HMAC-SHA256)

GitHub sends signature in X-Hub-Signature-256 header:

const crypto = require('crypto');

function verifyGitHubSignature(payload, signature, secret) {
const hmac = crypto.createHmac('sha256', secret);
hmac.update(payload, 'utf8');
const expected = 'sha256=' + hmac.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}

GitLab (Token)

GitLab sends token in X-Gitlab-Token header:

function verifyGitLabToken(token, secret) {
return token === secret;
}

IP Whitelisting

Recommended IP ranges to whitelist:

GitHub:

# Get latest from: https://api.github.com/meta
192.30.252.0/22
185.199.108.0/22
140.82.112.0/20

GitLab:

# GitLab.com
35.231.145.151

Manual Trigger Examples

Using cURL

# Check deployment status (no auth)
curl http://localhost:9001/status

# Check deployment status (with API key)
curl -H "Authorization: Bearer your_api_key" http://localhost:9001/status

# Check application health (always public)
curl http://localhost:9001/health

# Get build logs for latest release
curl http://localhost:9001/logs/1

# Get build logs with authentication
curl -H "Authorization: Bearer your_api_key" http://localhost:9001/logs/1

# Get last 50 lines of build log
curl "http://localhost:9001/logs/1?tail=50"

# Get build logs as JSON
curl "http://localhost:9001/logs/1?format=json"

# Trigger deployment
curl -X POST http://localhost:9001/webhook \
-H "Content-Type: application/json" \
-d '{"ref":"refs/heads/main"}'

# Trigger deployment with webhook signature verification
curl -X POST http://localhost:9001/webhook \
-H "Content-Type: application/json" \
-H "X-Hub-Signature-256: sha256=..." \
-d '{"ref":"refs/heads/main"}'

Using JavaScript

const crypto = require('crypto');
const axios = require('axios');

const baseUrl = 'http://localhost:9001';
const apiKey = 'your_api_key_here'; // Set to null if no auth required

// Create axios instance with optional auth
const apiClient = axios.create({
baseURL: baseUrl,
headers: apiKey ? { 'Authorization': `Bearer ${apiKey}` } : {}
});

// Check deployment status
async function checkStatus() {
const response = await apiClient.get('/status');
console.log('Deployment status:', response.data);
return response.data;
}

// Check health (always public, no auth needed)
async function checkHealth() {
const response = await axios.get(`${baseUrl}/health`);
console.log('Health:', response.data);
return response.data;
}

// Get build logs for a release
async function getBuildLogs(identifier, options = {}) {
const params = new URLSearchParams();
if (options.tail) params.append('tail', options.tail);
if (options.format) params.append('format', options.format);

const url = `/logs/${identifier}${params.toString() ? '?' + params.toString() : ''}`;
const response = await apiClient.get(url);
console.log('Build logs:', response.data);
return response.data;
}

// Trigger deployment with authentication
async function triggerDeploy() {
const secret = 'your_webhook_secret';
const payload = JSON.stringify({
ref: 'refs/heads/main',
repository: {
clone_url: 'https://github.com/user/repo.git'
}
});

// Calculate signature
const signature = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');

// Send webhook
const response = await axios.post(`${baseUrl}/webhook`, payload, {
headers: {
'Content-Type': 'application/json',
'X-Hub-Signature-256': signature
}
});

console.log('Deployment triggered:', response.data);
return response.data;
}

// Monitor deployment progress
async function monitorDeployment() {
let status = await checkStatus();

while (status.status === 'deploying') {
console.log(`Stage: ${status.stage}, Progress: ${status.progress_percent}%`);
await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5 seconds
status = await checkStatus();
}

console.log('Deployment completed!');
}

// Usage examples
await checkStatus();
await checkHealth();
await getBuildLogs(1); // Latest release
await getBuildLogs('001-abc123...'); // By release ID
await getBuildLogs(1, { tail: 50 }); // Last 50 lines
await getBuildLogs(1, { format: 'json' }); // JSON format
await triggerDeploy();
await monitorDeployment();

Using Python

import hashlib
import hmac
import json
import requests
import time

BASE_URL = 'http://localhost:9001'
API_KEY = 'your_api_key_here' # Set to None if no auth required

# Create session with optional auth headers
session = requests.Session()
if API_KEY:
session.headers.update({'Authorization': f'Bearer {API_KEY}'})

def check_status():
"""Check deployment status"""
response = session.get(f'{BASE_URL}/status')
data = response.json()
print('Deployment status:', data)
return data

def check_health():
"""Check application health (always public, no auth)"""
response = requests.get(f'{BASE_URL}/health')
data = response.json()
print('Health:', data)
return data

def get_build_logs(identifier, tail=None, format='text'):
"""Get build logs for a release"""
params = {}
if tail:
params['tail'] = tail
if format != 'text':
params['format'] = format

url = f'{BASE_URL}/logs/{identifier}'
response = session.get(url, params=params)

if format == 'json':
data = response.json()
print(f'Build logs (JSON): {len(data.get("lines", []))} lines')
return data
else:
print('Build logs (text):', response.text[:200], '...')
return response.text

def trigger_deploy():
"""Trigger deployment with authentication"""
secret = b'your_webhook_secret'
payload = {
'ref': 'refs/heads/main',
'repository': {
'clone_url': 'https://github.com/user/repo.git'
}
}

# Calculate signature
payload_bytes = json.dumps(payload).encode('utf-8')
signature = 'sha256=' + hmac.new(
secret,
payload_bytes,
hashlib.sha256
).hexdigest()

# Send webhook
response = requests.post(
f'{BASE_URL}/webhook',
json=payload,
headers={
'X-Hub-Signature-256': signature
}
)

print('Deployment triggered:', response.json())
return response.json()

def monitor_deployment():
"""Monitor deployment progress"""
status = check_status()

while status.get('status') == 'deploying':
stage = status.get('stage', 'unknown')
progress = status.get('progress_percent', 0)
print(f"Stage: {stage}, Progress: {progress}%")

time.sleep(5) # Wait 5 seconds
status = check_status()

print('Deployment completed!')

# Usage
if __name__ == '__main__':
check_status()
check_health()
get_build_logs(1) # Latest release
get_build_logs('001-abc123') # By release ID
get_build_logs(1, tail=50) # Last 50 lines
get_build_logs(1, format='json') # JSON format
trigger_deploy()
monitor_deployment()

Webhook Configuration

Environment Variables

VariableDefaultDescription
WEBHOOK_PORT9001Port for webhook listener
WEBHOOK_SECRET(empty)Secret for signature verification
WEBHOOK_SIGNATURE_VERIFYfalseEnable signature verification
WEBHOOK_PATH/webhookEndpoint path
WEBHOOK_TIMEOUT30000Request timeout (ms)

Example Configuration

docker run -d \
-e WEBHOOK_PORT=9001 \
-e WEBHOOK_SECRET=your_secret_here \
-e WEBHOOK_SIGNATURE_VERIFY=true \
-e WEBHOOK_PATH=/deploy \
-p 9001:9001 \
runonflux/orbit:latest

Deployment Queue

Flux-Orbit uses a single-slot pending deployment system to prevent conflicts:

  • Multiple webhooks within 5 seconds are debounced (configurable via DEBOUNCE_DELAY)
  • Only the latest commit is deployed (intermediate commits are skipped)
  • Deployments run sequentially (never concurrent)
  • If commits arrive during deployment, only the most recent one deploys next
  • Failed deployments don't block new deployments

Queue Management

# View queue status
curl http://localhost:9001/status | jq .queue

# Clear pending deployment
docker exec my-app rm -f /tmp/pending_commit

# Configure debounce delay (default: 5 seconds)
docker run -e DEBOUNCE_DELAY=10 ... # Wait 10 seconds
docker run -e DEBOUNCE_DELAY=0 ... # No debouncing (immediate)

How It Works

Example: Rapid commits during deployment

T=0s:   Push commit A → Deploys immediately
T=30s: Push commit B → Stored as pending
T=60s: Push commit C → Replaces B (B is skipped)
T=90s: Push commit D → Replaces C (C is skipped)
T=120s: A finishes → D deploys immediately (only latest)

Result: Only 2 deployments (A and D), intermediate commits skipped ✅

Rate Limiting

Flux-Orbit implements basic rate limiting:

  • Max 10 webhook requests per minute per IP
  • Configurable via WEBHOOK_RATE_LIMIT
  • Returns 429 Too Many Requests when exceeded

Monitoring

Using Status API

# Without authentication (if API_KEY not set)
curl http://localhost:9001/status | jq
curl http://localhost:9001/health | jq # Always public
curl http://localhost:9001/logs/1 | head -50

# With authentication (if API_KEY is set)
API_KEY="your_api_key_here"
curl -H "Authorization: Bearer $API_KEY" http://localhost:9001/status | jq
curl http://localhost:9001/health | jq # No auth needed for health
curl -H "Authorization: Bearer $API_KEY" http://localhost:9001/logs/1 | head -50

# Monitor deployment progress with authentication
API_KEY="your_api_key_here"
watch -n 2 "curl -s -H 'Authorization: Bearer $API_KEY' http://localhost:9001/status | jq"

# Get build logs for latest release
API_KEY="your_api_key_here"
curl -s -H "Authorization: Bearer $API_KEY" "http://localhost:9001/logs/1?tail=100"

# Get build logs as JSON with metadata
API_KEY="your_api_key_here"
curl -s -H "Authorization: Bearer $API_KEY" "http://localhost:9001/logs/1?format=json" | jq

# Monitor status, health, and recent build logs
API_KEY="your_api_key_here"
while true; do
echo "=== Status ==="
curl -s -H "Authorization: Bearer $API_KEY" http://localhost:9001/status | jq
echo "=== Health ==="
curl -s http://localhost:9001/health | jq
echo "=== Latest Build Log (last 20 lines) ==="
curl -s -H "Authorization: Bearer $API_KEY" "http://localhost:9001/logs/1?tail=20"
sleep 5
done

Webhook Logs

# View webhook activity
docker exec my-app tail -f /app/logs/webhook.log

# Count webhook calls
docker exec my-app grep "Webhook received" /app/logs/webhook.log | wc -l

# Failed verifications
docker exec my-app grep "verification failed" /app/logs/webhook.log

Deployment Monitoring

# Watch deployment progress in real-time (with auth)
API_KEY="your_api_key_here"
while true; do
STATUS=$(curl -s -H "Authorization: Bearer $API_KEY" http://localhost:9001/status)
STAGE=$(echo $STATUS | jq -r '.stage // "idle"')
PROGRESS=$(echo $STATUS | jq -r '.progress_percent // 0')
echo "$(date): Stage=$STAGE Progress=${PROGRESS}%"
sleep 2
done

# Alert when deployment completes and show build logs
API_KEY="your_api_key_here"
while true; do
STATUS=$(curl -s -H "Authorization: Bearer $API_KEY" http://localhost:9001/status | jq -r '.status')
if [ "$STATUS" != "deploying" ] && [ "$STATUS" != "building" ]; then
echo "Deployment completed!"

# Show last 100 lines of build log
echo "=== Build Log ==="
curl -s -H "Authorization: Bearer $API_KEY" "http://localhost:9001/logs/1?tail=100"

# Send notification (Slack, email, etc.)
break
fi
sleep 10
done

# Monitor build progress with live log streaming
API_KEY="your_api_key_here"
LAST_RELEASE=""
while true; do
STATUS=$(curl -s -H "Authorization: Bearer $API_KEY" http://localhost:9001/status)
CURRENT_STATUS=$(echo $STATUS | jq -r '.status')
CURRENT_RELEASE=$(echo $STATUS | jq -r '.current_release // "unknown"')

if [ "$CURRENT_STATUS" = "building" ]; then
STAGE=$(echo $STATUS | jq -r '.building_release.stage')
ELAPSED=$(echo $STATUS | jq -r '.building_release.elapsed_seconds')
echo "$(date): Building... Stage: $STAGE (${ELAPSED}s elapsed)"
elif [ "$CURRENT_RELEASE" != "$LAST_RELEASE" ] && [ "$LAST_RELEASE" != "" ]; then
echo "$(date): New release deployed: $CURRENT_RELEASE"
echo "=== Build Log ==="
curl -s -H "Authorization: Bearer $API_KEY" "http://localhost:9001/logs/1?tail=50"
LAST_RELEASE=$CURRENT_RELEASE
else
LAST_RELEASE=$CURRENT_RELEASE
fi

sleep 5
done

Metrics

Track these metrics for monitoring:

  • Webhook requests per minute
  • Successful deployments
  • Failed deployments
  • Average deployment time
  • Queue size
  • Deployment stage durations
  • Health check success rate

Troubleshooting

Common Issues

  1. 404 Not Found: Check WEBHOOK_PATH configuration
  2. 401 Unauthorized: Verify webhook secret matches
  3. 500 Internal Error: Check container logs
  4. Connection Refused: Ensure port 9001 is exposed

Debug Mode

Enable debug logging:

-e LOG_LEVEL=debug
-e DEBUG=webhook:*

Next Steps