Bring your own Server (BYOS)

Webhook Configuration

Integrate your own hosting solution into Laioutr by setting up a webhook. Cockpit calls this webhook for every deployment-related action.

General

Integrate your own hosting solution into Laioutr by setting up a webhook. Cockpit will call this webhook for every deployment-related action:

  • Deployments
  • Status Updates
  • Deployment Cancellation
  • Deployment Promotion
  • Rollbacks

You provide a URL that Cockpit will call for each of these actions.

Authentication (Standard Webhooks)

All requests from Cockpit are signed using the Standard Webhooks specification. When you configure your webhook, you'll receive a signing secret (prefixed with whsec_) that you must use to verify incoming requests.

Each request includes these headers:

HeaderDescription
webhook-idUnique identifier for this webhook delivery
webhook-timestampUnix timestamp (seconds) when the request was sent
webhook-signatureHMAC-SHA256 signature in format v1,{base64}

To verify a request:

  1. Concatenate {webhook-id}.{webhook-timestamp}.{body} (body is the raw request body)
  2. Remove the whsec_ prefix from your signing secret
  3. Base64-decode the remaining string to get the raw secret bytes
  4. Compute HMAC-SHA256 over the signed content using the decoded secret bytes
  5. Base64-encode the result and compare with the signature after the v1, prefix (timing-safe comparison)
  6. Reject requests older than 5 minutes to prevent replay attacks

Most languages have Standard Webhooks libraries available. See standardwebhooks.com for implementations.

Request Format

All requests are POST with Content-Type: application/json. Every request includes:

FieldTypeDescription
eventstringThe event type (e.g., hosting.deployment.created)
timestampstringISO 8601 timestamp (UTC, e.g., 2025-01-30T12:00:00.000Z) of when the http-request was sent
projectstringProject identifier as org-slug/project-slug
dataobjectEvent-specific payload (optional)
{
  "event": "hosting.deployment.created",
  "timestamp": "2025-01-30T12:00:00.000Z",
  "project": "acme-corp/storefront",
  "data": { ... }
}

TypeScript Types

TypeScript definitions for all webhook events and responses are available in the @laioutr/webhook-types package:

npm install @laioutr/webhook-types

Usage example:

import type { ByosWebhookEvent, ByosDescribeResponse, ByosWebhookResponse } from '@laioutr/webhook-types/byos';

function handleWebhook(event: ByosWebhookEvent): ByosWebhookResponse {
  if (event.event === 'hosting.describe') {
    return {
      ok: true,
      data: {
        name: 'My CI/CD System',
        url: 'https://storefront.example.com',
        capabilities: {
          /* ... */
        },
      },
    } satisfies ByosDescribeResponse;
  }
  if (event.event === 'hosting.deployment.created') {
    const { deploymentId, callbackUrl, files } = event.data;
    startBuild(deploymentId, files, callbackUrl);
  }
  return { ok: true, data: {} };
}

Delivery Behavior

Retries

Cockpit retries failed webhook deliveries with the following policy:

ParameterValue
Max attempts3
Per-attempt timeout7 seconds
Total time budget25 seconds
Retry delayExponential backoff (1s, 2s, 4s) with 30% jitter

A delivery is considered failed if:

  • The endpoint returns a non-2xx HTTP status
  • The JSON payload is not valid
  • The request times out
  • A network error occurs

Returning { "ok": false, "error": "..." } will not trigger a retry.

If the retries did not succeed, the event will be discarded.

Idempotency

The webhook-id header serves as an idempotency key. The same webhook-id is used across all retry attempts for a given event. Your endpoint should use this to deduplicate requests if needed.

First attempt:  webhook-id: evt_abc123
Retry 1:        webhook-id: evt_abc123  (same)
Retry 2:        webhook-id: evt_abc123  (same)

You can track webhook-id values to skip duplicates.

Response Format

Your endpoint must respond with JSON in this format:

{
  "ok": true,
  "data": { ... }
}

Or on failure:

{
  "ok": false,
  "error": "Human-readable error message"
}

Return appropriate HTTP status codes:

  • 200 for successful processing
  • 400 for invalid requests
  • 401 for authentication failures
  • 500 for server errors

Events

hosting.describe

Cockpit sends this event to discover your system's capabilities. This is called during initial setup and periodically to refresh capabilities.

Request

{
  "event": "hosting.describe",
  "timestamp": "2025-01-30T12:00:00.000Z",
  "project": "acme-corp/storefront"
}

Response

{
  "ok": true,
  "data": {
    "name": "Your CI/CD System",
    "url": "https://storefront.example.com",
    "capabilities": {
      "statusUpdates": true,
      "cancelDeployment": false,
      "promoteDeployment": false,
      "rollbackDeployment": false,
      "deleteDeployment": false
    }
  }
}

Fields

FieldDescription
nameDisplay name for your hosting provider (shown in Cockpit UI)
urlBase URL where the project is hosted (e.g., https://storefront.example.com). Will be used in the studio preview
capabilitiesObject describing which actions your system supports

Capabilities

CapabilityDescription
statusUpdatesYour system will call back with deployment status updates. If this capability is not supported, the Cockpit deployment status will be set to unknown.
cancelDeploymentYour system can cancel in-progress deployments
promoteDeploymentYour system can promote deployments to production
rollbackDeploymentYour system can rollback to previous deployments
deleteDeploymentYour system can delete deployments

Set capabilities to true only for actions your system supports. Cockpit will only send those event types if you indicate support.

hosting.connected

Sent when a project successfully connects to your webhook. Use this to set up any resources you need for the project.

Request

{
  "event": "hosting.connected",
  "timestamp": "2025-01-30T12:00:00.000Z",
  "project": "acme-corp/storefront"
}

Response

{
  "ok": true,
  "data": {}
}

hosting.disconnected

Sent when a project disconnects from your webhook. Use this to clean up any resources.

Request

{
  "event": "hosting.disconnected",
  "timestamp": "2025-01-30T12:00:00.000Z",
  "project": "acme-corp/storefront"
}

Response

{
  "ok": true,
  "data": {}
}

hosting.deployment.created

Sent when a user triggers a deployment. Contains all files needed to build and deploy the project.

Request

{
  "event": "hosting.deployment.created",
  "timestamp": "2025-01-30T12:00:00.000Z",
  "project": "acme-corp/storefront",
  "data": {
    "deploymentId": "dep_abc123",
    "environment": "production",
    "callbackUrl": "https://cockpit.laioutr.cloud/api/webhook/hosting/dep_abc123?secret=cbsec_xxx",
    "files": {
      "package.json": "{ \"name\": \"storefront\", ... }",
      "nuxt.config.ts": "export default defineNuxtConfig({ ... })",
      "laioutrrc.json": "{ ... }",
      "app.vue": "<template>...</template>"
    }
  }
}

Fields

FieldDescription
deploymentIdUnique identifier for this deployment
environmentEither "production" or "staging"
callbackUrlURL to POST status updates (see Status Callbacks)
filesMap of filename to file contents

Response

Acknowledge receipt immediately. Don't wait for the build to complete.

{
  "ok": true,
  "data": {}
}

hosting.deployment.cancel

Sent when a user requests to cancel an in-progress deployment. Only sent if you indicated cancelDeployment: true in capabilities.

Request

{
  "event": "hosting.deployment.cancel",
  "timestamp": "2025-01-30T12:00:00.000Z",
  "project": "acme-corp/storefront",
  "data": {
    "deploymentId": "dep_abc123"
  }
}

Response

{
  "ok": true,
  "data": {}
}

hosting.deployment.promote

Sent when a user wants to promote a staging deployment to production. Only sent if you indicated promoteDeployment: true in capabilities.

Request

{
  "event": "hosting.deployment.promote",
  "timestamp": "2025-01-30T12:00:00.000Z",
  "project": "acme-corp/storefront",
  "data": {
    "deploymentId": "dep_abc123"
  }
}

Response

{
  "ok": true,
  "data": {}
}

hosting.deployment.rollback

Sent when a user wants to rollback to a previous deployment. Only sent if you indicated rollbackDeployment: true in capabilities.

Request

{
  "event": "hosting.deployment.rollback",
  "timestamp": "2025-01-30T12:00:00.000Z",
  "project": "acme-corp/storefront",
  "data": {
    "deploymentId": "dep_abc123",
    "fromDeploymentId": "dep_xyz789"
  }
}

Fields

FieldRequiredDescription
deploymentIdYesDeployment to roll back TO
fromDeploymentIdNoCurrently active deployment being replaced

Response

{
  "ok": true,
  "data": {}
}

hosting.deployment.delete

Sent when a user wants to delete a deployment. Only sent if you indicated deleteDeployment: true in capabilities.

Request

{
  "event": "hosting.deployment.delete",
  "timestamp": "2025-01-30T12:00:00.000Z",
  "project": "acme-corp/storefront",
  "data": {
    "deploymentId": "dep_abc123"
  }
}

Response

{
  "ok": true,
  "data": {}
}

Status Callbacks

If you set statusUpdates: true in your capabilities, you should POST status updates to the callbackUrl provided in the deployment request.

Callback URL

The callback URL is provided in the hosting.deployment.created event:

https://cockpit.laioutr.cloud/api/webhook/hosting/{deploymentId}?secret={secret}

The deploymentId is embedded in the URL path. The secret parameter (prefixed with cbsec_) authenticates your request. No additional headers or signatures are required.

Deployment Status State Machine

The following diagram shows the valid deployment status transitions:

State Transition Rules:

  • canceled is a terminal state - no transitions are allowed after cancellation
  • Same status updates are ignored (no-op)
  • All other transitions are allowed, including recovery from error back to running
  • Invalid transitions are silently accepted but not applied

Status Events

Send a POST request with Content-Type: application/json.

Note: Status callbacks do not include the project field. The deployment is identified by the deploymentId in the callback URL path.

Running

Indicate that the deployment is in progress:

{
  "event": "hosting.deployment.status",
  "timestamp": "2025-01-30T12:05:00.000Z",
  "data": {
    "status": "running"
  }
}

Success

Indicate that deployment succeeded. The url field is required and must be a valid URL:

{
  "event": "hosting.deployment.status",
  "timestamp": "2025-01-30T12:10:00.000Z",
  "data": {
    "status": "success",
    "url": "https://storefront.example.com"
  }
}

Error

Indicate that the deployment failed. The error field is required:

{
  "event": "hosting.deployment.status",
  "timestamp": "2025-01-30T12:10:00.000Z",
  "data": {
    "status": "error",
    "error": "Build failed: npm install returned exit code 1"
  }
}

Cancelled

Indicate that the deployment was canceled:

{
  "event": "hosting.deployment.status",
  "timestamp": "2025-01-30T12:08:00.000Z",
  "data": {
    "status": "canceled"
  }
}

Indicate that a deployment was promoted to production. The url field is optional:

{
  "event": "hosting.deployment.status",
  "timestamp": "2025-01-30T12:15:00.000Z",
  "data": {
    "status": "promoted",
    "url": "https://storefront.example.com"
  }
}

Callback Response

Cockpit responds with:

{
  "ok": true,
  "data": {}
}

Or on error:

{
  "ok": false,
  "error": "Deployment not found"
}

Callback HTTP Status Codes

StatusMeaning
200Status update accepted
400Invalid payload format
401Missing or invalid callback secret
404Deployment not found
500Server error

Note: Invalid status transitions (e.g., updating a canceled deployment) return 200 with { "ok": true } but are silently ignored.

Retry Recommendations

If Cockpit is temporarily unavailable when sending status callbacks:

  • Use exponential backoff (e.g., 1s, 2s, 4s, 8s, up to 5 minutes)
  • Repeated identical status updates are safe (idempotent)
  • After extended failures, consider logging the issue for manual review

Setup in Cockpit

  1. Go to ProjectHosting
  2. Click Connect custom hosting
  3. Enter your webhook endpoint URL
  4. Copy the signing secret (starts with whsec_) and configure it in your system
  5. Click Test connection to verify everything works
  6. Click Confirm to save the configuration

Your webhook will now receive events for all deployment actions.

Troubleshooting

Signature verification fails

  • Ensure you're using the raw request body for verification, not a parsed JSON object
  • Remove the whsec_ prefix from the secret
  • Base64-decode the secret before using it as the HMAC key (this is required by Standard Webhooks)
  • The signature format is v1,{base64} - extract the base64 part after v1, for comparison
  • Check that your signing secret matches exactly (no extra whitespace)
  • Verify the timestamp is within 5 minutes of the current time
  • Consider using a Standard Webhooks library for your language - see standardwebhooks.com

Not receiving events

  • Check that your endpoint is publicly accessible
  • Verify your endpoint returns 200 status codes
  • Check your server logs for errors

Deployment stuck in "running"

  • Ensure you're calling the callback URL with status updates
  • Verify the callback URL secret (cbsec_ prefix) is included in the query string
  • Check that your status payload matches the expected format
  • The url field is required for success status - invalid URLs are rejected with 400