Webhook Configuration
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.
@laioutr/byos-agent handles webhook verification, script execution, and status callbacks out of the box. See the BYOS Agent documentation to get started quickly, including deployment examples for Docker and PM2.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:
| Header | Description |
|---|---|
webhook-id | Unique identifier for this webhook delivery |
webhook-timestamp | Unix timestamp (seconds) when the request was sent |
webhook-signature | HMAC-SHA256 signature in format v1,{base64} |
To verify a request:
- Concatenate
{webhook-id}.{webhook-timestamp}.{body}(body is the raw request body) - Remove the
whsec_prefix from your signing secret - Base64-decode the remaining string to get the raw secret bytes
- Compute HMAC-SHA256 over the signed content using the decoded secret bytes
- Base64-encode the result and compare with the signature after the
v1,prefix (timing-safe comparison) - 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:
| Field | Type | Description |
|---|---|---|
event | string | The event type (e.g., hosting.deployment.created) |
timestamp | string | ISO 8601 timestamp (UTC, e.g., 2025-01-30T12:00:00.000Z) of when the http-request was sent |
project | string | Project identifier as org-slug/project-slug |
data | object | Event-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:
| Parameter | Value |
|---|---|
| Max attempts | 3 |
| Per-attempt timeout | 7 seconds |
| Total time budget | 25 seconds |
| Retry delay | Exponential 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:
200for successful processing400for invalid requests401for authentication failures500for 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
| Field | Description |
|---|---|
name | Display name for your hosting provider (shown in Cockpit UI) |
url | Base URL where the project is hosted (e.g., https://storefront.example.com). Will be used in the studio preview |
capabilities | Object describing which actions your system supports |
Capabilities
| Capability | Description |
|---|---|
statusUpdates | Your system will call back with deployment status updates. If this capability is not supported, the Cockpit deployment status will be set to unknown. |
cancelDeployment | Your system can cancel in-progress deployments |
promoteDeployment | Your system can promote deployments to production |
rollbackDeployment | Your system can rollback to previous deployments |
deleteDeployment | Your 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
| Field | Description |
|---|---|
deploymentId | Unique identifier for this deployment |
environment | Either "production" or "staging" |
callbackUrl | URL to POST status updates (see Status Callbacks) |
files | Map 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
| Field | Required | Description |
|---|---|---|
deploymentId | Yes | Deployment to roll back TO |
fromDeploymentId | No | Currently 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:
canceledis a terminal state - no transitions are allowed after cancellation- Same status updates are ignored (no-op)
- All other transitions are allowed, including recovery from
errorback torunning - 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"
}
}
Promoted
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
| Status | Meaning |
|---|---|
| 200 | Status update accepted |
| 400 | Invalid payload format |
| 401 | Missing or invalid callback secret |
| 404 | Deployment not found |
| 500 | Server 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
- Go to Project → Hosting
- Click Connect custom hosting
- Enter your webhook endpoint URL
- Copy the signing secret (starts with
whsec_) and configure it in your system - Click Test connection to verify everything works
- 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 afterv1,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
200status 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
urlfield is required forsuccessstatus - invalid URLs are rejected with400