Skip to content
GitHub

Incidents and webhooks

A failed deployment (severity critical) or build (warning) opens an incident for its app. If it fails again, Kuben adds to the same open incident instead of opening a new one. The next success resolves it. Stale database backups open an organization-wide incident, which Kuben resolves once a fresh backup exists; it shows on the Incidents page but sends no webhook.

Terminal window
curl -H "Authorization: Bearer $TOKEN" https://kuben.example.com/api/v1/incidents
  • Acknowledge an incident (POST /api/v1/incidents/{id}/acknowledge) to show that someone is working on it.
  • Resolve it by hand (POST /api/v1/incidents/{id}/resolve).
  • Add ?all=true to the list request to include resolved incidents.

Every member can see incidents; acknowledging and resolving need the developer role or higher. Resolved incidents are deleted after 180 days (retention.resolved_incident_days).

A silence mutes incident webhooks for an environment or one app, for up to 7 days; the incidents are still recorded, and deployment.* and build.* events are still sent. On the environment page, under Alert silences, press Add a silence, give a Reason and an end, and choose what it Applies to: the whole environment or one app. Lift ends it early. Silences need the developer role or higher; the API is POST /api/v1/projects/{project}/environments/{env}/silences.

Owners add endpoints on the Webhooks page, or with POST /api/v1/webhooks from a signed-in session (API tokens are refused), with a name, an https:// URL and a list of events (* means all events):

Event When
deployment.started, deployment.succeeded, deployment.failed, deployment.cancelled A deployment is accepted or settles
build.started, build.succeeded, build.failed, build.cancelled A Git build is queued or settles
incident.opened, incident.resolved An incident changes
ping You ask for one: POST /api/v1/webhooks/{id}/ping

The response shows the endpoint’s signing secret (whsec_…). This is the only time Kuben shows it.

  • Retries. Kuben tries a delivery up to 12 times, waiting longer each time (from one minute up to six hours), and gives up after a day. Any 2xx answer within 10 seconds counts as delivered.
  • Disabling. An endpoint is disabled after 20 deliveries in a row have used up their attempts.
  • Stale events. Events more than an hour old when Kuben comes back from an outage are not sent.
  • Deduplication. Each event has a unique Kuben-Delivery id; use it to ignore repeats.

To see recent deliveries, call GET /api/v1/webhooks/{id}/deliveries. To send a failed delivery again right away, call POST /api/v1/webhooks/{id}/deliveries/{delivery}/retry. Only failed deliveries of enabled endpoints can be retried; the retry gets the full number of attempts again, as long as the event is less than a day old. Deliveries are kept for 30 days (retention.webhook_delivery_days).

Incidents in the sidebar lists open incidents (tick Show resolved to include the rest) with their severity, a Runbook link, when they opened and were last seen, and Acknowledge and Resolve buttons. Home shows the open ones too.

Webhooks in the sidebar adds endpoints (Add a webhook) and shows the signing secret once. For each endpoint, Deliveries lists what was sent with its HTTP status and error, Retry sends a failed one again, and Send a test sends a ping. Delete webhook disables an endpoint for good. Both pages are available in English and Persian.

Every request carries a signature header:

Kuben-Signature: t=1758103200,v1=5f2c…

v1 is the hex HMAC-SHA256, keyed with the secret, of <t>.<raw body>. To verify a request:

  1. Recompute the HMAC over the raw request body.
  2. Compare it to v1 in constant time.
  3. Reject timestamps older than a few minutes.
import { createHmac, timingSafeEqual } from 'node:crypto'
export function verify(secret: string, header: string, body: Buffer): boolean {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
const age = Math.abs(Date.now() / 1000 - Number(parts.t))
if (!parts.v1 || age > 300) return false
const mac = createHmac('sha256', secret).update(`${parts.t}.`).update(body).digest()
const given = Buffer.from(parts.v1, 'hex')
return given.length === mac.length && timingSafeEqual(given, mac)
}

For apps that deploy from a GitHub repository, Kuben reports a commit status on the commit:

  • kuben/build for builds;
  • kuben/<environment> for deployments.

The status is pending when the work starts and success, failure or error when it ends. It links to the app’s page when server.public_url is set. Kuben’s GitHub App needs the Commit statuses: write permission.