Skip to content

Webhooks and events

Everything that happens in Slipway is an event on an internal stream. Events feed notifications, the audit log, metering and outbound webhooks.

{
"id": "01H...",
"org_id": "org_01H...",
"type": "deployment.succeeded",
"at": "2026-10-21T10:15:00Z",
"actor": { "type": "user", "id": "user_01H...", "name": "Ada" },
"data": { "deployment_id": "dep_01H...", "environment": "production", "release": "1.4.0" }
}

Event types follow <domain>.<action>, for example run.started, run.finished, job.queued, deployment.waiting_approval, deployment.succeeded, deployment.failed, deployment.rolled_back, release.created, runner.offline.

Create a webhook with a URL and the events you want (WebhookService.CreateWebhook, or Settings, Webhooks). The signing secret is shown once.

Each delivery is an HTTPS POST with JSON and these headers:

Header Value
X-Slipway-Event The event type
X-Slipway-Delivery A unique delivery id (use it to de-duplicate)
X-Slipway-Signature t=<unix seconds>,v1=<hex HMAC-SHA256>

The signature is the HMAC-SHA256, keyed with your webhook secret, of the string "<t>.<raw body>". Recompute it, compare in constant time and reject old timestamps.

func verify(secret, header string, body []byte) bool {
var t, v1 string
for _, p := range strings.Split(header, ",") {
k, v, _ := strings.Cut(p, "=")
switch k { case "t": t = v; case "v1": v1 = v }
}
ts, err := strconv.ParseInt(t, 10, 64)
if err != nil || time.Since(time.Unix(ts, 0)).Abs() > 5*time.Minute { return false }
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(t + "."))
mac.Write(body)
want, _ := hex.DecodeString(v1)
return hmac.Equal(mac.Sum(nil), want)
}
import crypto from "node:crypto";
export function verify(secret, header, rawBody) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const mac = crypto.createHmac("sha256", secret).update(`${parts.t}.`).update(rawBody).digest();
const want = Buffer.from(parts.v1, "hex");
return want.length === mac.length && crypto.timingSafeEqual(mac, want)
&& Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
}
  • Respond with any 2xx within a few seconds to acknowledge.
  • Failures are retried with backoff. Every attempt (status, response, timing) is kept in the delivery log (ListWebhookDeliveries).
  • Redeliver any delivery manually (RedeliverWebhook) and send a test event (PingWebhook).
  • Rotate the secret with RotateWebhookSecret.
  • Handlers should be idempotent: the same delivery id can arrive more than once.
Path Sender Verification
POST /hooks/scm/{provider}/{connection_id} GitHub, GitLab, Bitbucket, Azure Repos, Gitea Provider signature or token, checked before parsing
POST /hooks/integrations/{provider}/{integration_id} Third-party integrations Provider-specific
POST /hooks/generic/{pipeline_id} Anything, to trigger a pipeline with the webhook trigger HMAC with the trigger secret

All inbound webhooks are verified before the body is parsed, and bodies are size-limited.

Notification and incident integrations use subscriptions: an event filter plus a target (channel, routing key, project key). See Integrations.

Besides organisation-wide subscriptions, each member has an inbox (the bell) and per-organisation preferences under Settings → Notifications. The notifications worker turns events into entries for the people they concern, in the app and by email:

Preference Who receives it
Run failed The user who started the run
Run fixed The user who started a run that succeeds after the pipeline’s previous run failed
Approval requested Members allowed to approve deployments
Deployment finished The user who started the deployment (succeeded, failed, rolled back or cancelled)
Runner offline Members who manage runners
Usage threshold Members who manage billing, plus the extra recipients in the billing alert settings

NotificationService exposes the same inbox and preferences to API clients (ListNotifications, MarkNotificationsRead, WatchNotifications, GetNotificationPreferences, UpdateNotificationPreferences). Entries are kept for 90 days.