Deploy webhooks
Get a signed HTTP request when a deploy starts, succeeds or fails or a service crashes, verify it, and handle retries, tests and redelivery.
A deploy webhook sends a signed POST to your own URL when something happens to a deployment in a project, so you can post results to Slack, update a status page or start your own tooling. Each project can have up to 20 webhooks.
Add a webhook
Open the project's Settings and choose Add webhook under Deploy webhooks. Enter the URL, choose the events it should receive and choose Add webhook. Dekka then shows the signing secret, which starts with whsec_, once, so store it where your endpoint can read it.
The URL must use https and point at a public address, and it can't contain a username or password. Dekka refuses anything else with 422 invalid_webhook_url. Admins and owners can add, change and delete webhooks, and developers can send tests and redeliver.
Which events are sent?
| Event | Shown as | Sent when |
|---|---|---|
deployment.started |
Deploy started | A deployment leaves queued and starts preparing, building or deploying |
deployment.succeeded |
Deploy succeeded | A deployment passes its health check and becomes healthy |
deployment.failed |
Deploy failed | A deployment fails |
deployment.crashed |
Service crashed | A running service is reported unhealthy, sent with the deployment that is serving |
A cancelled deployment sends nothing. Choose Turn off next to a webhook to pause it, and deliveries that were waiting to be sent fail instead of going out.
What does a delivery look like?
Each delivery is a POST with a JSON body and these headers:
| Header | Value |
|---|---|
X-Dekka-Event |
The event, such as deployment.failed |
X-Dekka-Delivery |
The delivery's ID, which stays the same through retries and redeliveries |
X-Dekka-Signature |
t=<unix seconds>,v1=<hex>, described below |
User-Agent |
Dekka-Webhooks/1 |
The body has the delivery's id, the event as type, when it happened as created_at, and the deployment with its service, environment, project and organisation under data.
{
"created_at": "2026-11-04T09:12:30.512088+00:00",
"data": {
"deployment": {
"branch": "main",
"commit_message": "Fix the checkout total",
"commit_sha": "4f2a9c1e8b7d6a5f4e3d2c1b0a9f8e7d6c5b4a39",
"created_at": "2026-11-04T09:12:28.904113+00:00",
"error": null,
"finished_at": null,
"id": "…",
"image_ref": null,
"status": "preparing",
"trigger": "push"
},
"environment": { "id": "…", "name": "production" },
"organization": { "id": "…", "slug": "acme" },
"project": { "id": "…", "name": "Shop" },
"resource": { "id": "…", "kind": "application", "name": "api" }
},
"id": "…",
"type": "deployment.started"
}
status and trigger take the values in Deployments. For deployment.failed, error holds the failure's code, message and the phase it failed in, such as building. image_ref is the image that was deployed, which a deployment from GitHub records once it's healthy.
Verify the signature
Check every request before you trust it. Dekka signs the raw body with HMAC-SHA256, using the whole secret, whsec_ included, as the key. The signed message is the timestamp from t, a full stop, then the body exactly as it arrived, and v1 is the result in lowercase hex.
Compute the same value and compare it in constant time, and reject a request whose t is more than five minutes from your clock so an old request can't be replayed. Each attempt is signed again with a fresh timestamp. Verify the bytes you received, because parsing the JSON and serialising it again changes them.
import crypto from "node:crypto";
import express from "express";
const secret = process.env.DEKKA_WEBHOOK_SECRET;
function verify(header, body) {
const parts = header.split(",").map((p) => p.trim().split("="));
const t = Number(parts.find(([k]) => k === "t")?.[1]);
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = Buffer.from(crypto.createHmac("sha256", secret).update(`${t}.`).update(body).digest("hex"));
return parts
.filter(([k]) => k === "v1")
.some(([, v]) => v.length === expected.length && crypto.timingSafeEqual(Buffer.from(v), expected));
}
const app = express();
app.post("/hooks/dekka", express.raw({ type: "application/json" }), (req, res) => {
if (!verify(req.get("X-Dekka-Signature") ?? "", req.body)) return res.sendStatus(400);
const event = JSON.parse(req.body);
console.log(event.type, event.data.deployment.status);
res.sendStatus(204);
});
app.listen(3000);import hashlib
import hmac
import os
import time
from fastapi import FastAPI, HTTPException, Request
SECRET = os.environ["DEKKA_WEBHOOK_SECRET"]
app = FastAPI()
def verify(body: bytes, header: str, tolerance: int = 300) -> bool:
parts: dict[str, list[str]] = {}
for item in header.split(","):
key, _, value = item.strip().partition("=")
parts.setdefault(key, []).append(value)
try:
timestamp = int(parts["t"][0])
except (KeyError, ValueError):
return False
if abs(time.time() - timestamp) > tolerance:
return False
expected = hmac.new(SECRET.encode(), f"{timestamp}.".encode() + body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, v) for v in parts.get("v1", []))
@app.post("/hooks/dekka", status_code=204)
async def dekka_webhook(request: Request) -> None:
body = await request.body()
if not verify(body, request.headers.get("X-Dekka-Signature", "")):
raise HTTPException(status_code=400)
event = await request.json()
print(event["type"], event["data"]["deployment"]["status"])What if my endpoint is down?
Dekka counts any 2xx answer as delivered. Anything else counts as a failure, including a redirect, which Dekka doesn't follow, and no answer within 5 seconds. Dekka tries again after 30 seconds and waits twice as long before each later try. After 8 attempts, about an hour after the first, the delivery is marked failed.
A delivery can arrive more than once, so use X-Dekka-Delivery to ignore one you've already handled. Answer quickly and do slow work after you respond, so a busy handler isn't mistaken for a failure.
Send a test event
Choose Send test next to a webhook. Dekka sends a webhook.test event straight away, even when the webhook is turned off, and the result appears in its deliveries. A test isn't retried if it fails.
{
"created_at": "2026-11-04T09:20:02.118734+00:00",
"data": {
"message": "This is a test delivery from Dekka.",
"project": { "id": "…", "name": "Shop" },
"webhook_id": "…"
},
"id": "…",
"type": "webhook.test"
}
Redeliver an event
Select a webhook to see its recent deliveries, with the status your endpoint answered and how many attempts each took. Choose Redeliver on a failed one and Dekka sends it again with the same X-Dekka-Delivery ID and body and a fresh set of attempts. The API also redelivers one that succeeded, with POST /v1/webhook-deliveries/{id}/redeliver, and answers 409 delivery_pending while a delivery is still being sent.
Rotate the secret
Select the webhook and choose Rotate secret. Dekka shows the new secret once, and signs every delivery with it from then on, including ones still waiting to be retried. The old secret stops working at once, so have your endpoint accept a signature from either secret until you've deployed the new one.
Delete removes the webhook and its delivery history.