Integrating
Get told when things happen, and let your app announce what it can do.
An outbound webhook is a URL of yours that ClikDeploy POSTs to when something changes on your account — a deploy finishes, an app goes down, a server drops off. An event is one of those things happening.
Settings → Integrations → Webhooks, or over the API. events is a filter list; leave it out to receive everything.
terminal
curl -X POST https://clikdeploy.com/api/webhooks/endpoints \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "prod relay",
"url": "https://example.com/hooks/clikdeploy",
"events": ["deployment.status.changed", "app.*"]
}'The response contains a secret. It is shown once and never again — store it before you close the window. A filter matches an exact event type, a prefix wildcard like app.*, or * for everything.
| Event type | When |
|---|---|
deployment.status.changed | A deployment moved to a new status, including finished and failed. |
app.status.changed | An app changed status — started, stopped, unhealthy. |
app.url.ready | An app is running and reachable on its domain. |
server.status.changed | A server changed status. |
agent.status.changed | The agent on a server went online or offline. |
slo.breach.opened | An SLO breach was opened. |
webhook.test | You pressed test. Sent down the same signed path as a real event. |
Every delivery is the same envelope. data is the only part that varies by event type.
POST body
{
"id": "b6b2f0e6-0e1a-4c3b-9a1e-2f4c8d5e7a90",
"type": "deployment.status.changed",
"createdAt": "2026-07-28T09:14:02.117Z",
"data": {
"deploymentId": "...",
"appId": "...",
"appName": "api",
"status": "SUCCESS",
"error": null
}
}These headers come with it:
| Header | Value |
|---|---|
X-ClikDeploy-Event | The event type, same as type. |
X-ClikDeploy-Delivery | The delivery id, same as id. Use it to de-duplicate. |
X-ClikDeploy-Signature-256 | sha256= followed by the hex HMAC of the raw body. |
The signature is HMAC-SHA256 of the raw request body, keyed by your endpoint secret, hex-encoded, prefixed with sha256=. Compare it in constant time.
Node.js — Express
import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';
const app = express();
// Raw body, not express.json() — the bytes must be exactly what was signed.
app.post('/hooks/clikdeploy', express.raw({ type: 'application/json' }), (req, res) => {
const received = req.get('X-ClikDeploy-Signature-256') || '';
const expected =
'sha256=' + createHmac('sha256', process.env.CLIKDEPLOY_WEBHOOK_SECRET).update(req.body).digest('hex');
const a = Buffer.from(received);
const b = Buffer.from(expected);
if (a.length !== b.length || !timingSafeEqual(a, b)) {
return res.status(401).send('bad signature');
}
const event = JSON.parse(req.body.toString('utf8'));
console.log(event.type, event.data);
res.sendStatus(200);
});Sign the bytes you received
Anything other than a 2xx counts as a failure. Each delivery is retried inline three times (immediately, then after 1.5s, then after 5s), and a failure that survives those is picked up again later by a background retry sweep. Each attempt times out after 10 seconds. Because of retries your handler must be idempotent — de-duplicate on id.
Every attempt is recorded, with the response status and body. Read them in Settings → Integrations, or:
terminal
curl "https://clikdeploy.com/api/webhooks/deliveries?endpointId=ENDPOINT_ID&success=false" \ -H "X-API-Key: YOUR_API_KEY"
A test fires a real webhook.test event down the real signed path, so it proves your verification works rather than just that the URL resolves.
terminal
curl -X POST https://clikdeploy.com/api/webhooks/endpoints/ENDPOINT_ID/test \ -H "X-API-Key: YOUR_API_KEY"
Rotating replaces the secret immediately and returns the new one once. Deliveries after that point are signed with the new secret, so update your handler in the same change.
terminal
curl -X POST https://clikdeploy.com/api/webhooks/endpoints/ENDPOINT_ID/rotate-secret \ -H "X-API-Key: YOUR_API_KEY"
Endpoint URLs must be public HTTPS or HTTP addresses; private and loopback addresses are rejected when you save the endpoint and again at delivery time.
This is the other direction: your Git host tells ClikDeploy that you pushed, and the app rebuilds and redeploys. GitHub, GitLab and Bitbucket are supported.
autoDeploy when you create it. Enabling it at create time registers the push webhook on the repo for you.Pushes to other branches are ignored. If a build does not start, check the app is tracking the branch you pushed, and that the hook still exists on the repo — the webhook registration is best-effort at create time and can be added by hand later.
You can also trigger a deploy yourself at any time from the CLI or the REST API.
An event manifest is a small JSON file where your app declares the events it emits and the webhook paths it accepts. It sits at /.well-known/clikdeploy/events.json inside your app.
The ClikDeploy agent on the server reads it when your container starts, so the platform knows what your app can send and receive without anyone typing it into a form. It is optional — an app with no manifest deploys and runs exactly the same.
/.well-known/clikdeploy/events.json
{
"name": "orders",
"displayName": "Orders service",
"emits": [
{ "type": "order.created", "description": "A customer completed checkout" }
],
"consumes": [
{ "event": "payment.settled", "path": "/hooks/payment", "method": "POST" }
]
}Serving that file yourself is the reliable route, and works in any language. If you are on Node, the clikevents package ships a handler that builds the manifest from declarations you make in code and serves it at the well-known path. It is distributed with the platform rather than from the public npm registry, so ask us for it before wiring these in.
Express / Connect / Koa
import { middleware } from 'clikevents/middleware/connect';
app.use(middleware());Fastify
import clikevents from 'clikevents/middleware/fastify'; fastify.register(clikevents);
Next.js — app/.well-known/clikdeploy/events.json/route.ts
export { clikeventsHandler as GET } from 'clikevents/middleware/next';If you cannot add a route — a third-party image, say — declare the manifest as a Docker label instead. The agent reads clikdeploy.events.manifest as inline JSON.