# Webhooks, Slack and Teams

> Get deploys, outages, backups, incidents and invoices pushed to your own systems or a chat channel.

Source: https://veldhost.eu/docs/webhooks-and-integrations

veldhost can push what happens to your sites to your own systems — a CI pipeline, an incident tool, a dashboard — or straight into a Slack or Microsoft Teams channel. Every event goes through the same pipeline: signed, retried, and logged where you can see it. Set it up under [Account → Integrations](https://manage.veldhost.eu/portal/integrations).

## Events

Pick any of these when you add an endpoint. Events cover every site on your account, not just your own.

```
deploy.succeeded         a deploy finished and the new version is serving
deploy.failed            a deploy failed; the previous version keeps serving
backup.completed         a backup finished (data.ok says whether it succeeded)
restore.completed        a restore you asked for finished (data.ok)
site.down                a site has been failing its health probe for 15 minutes
site.up                  it is answering again (data.downtime_minutes)
incident.opened          we opened a customer-visible incident touching your site
incident.resolved        that incident was resolved
domain.expiring          a registered domain is 30 or 7 days from expiry, or has expired
invoice.paid             an invoice was paid
invoice.payment_failed   a payment failed — update your card to keep your sites online
webhook.test             sent by the "Send test event" button; never subscribable
```

## What a delivery looks like

A generic webhook receives one HTTP `POST` per event with a JSON body:

```
POST /your/receiver HTTP/1.1
Content-Type: application/json
X-Veldhost-Event: deploy.succeeded
X-Veldhost-Delivery: 7c1a0e2e-5b0b-4f0f-9a5e-2c6b6a1d3f10
X-Veldhost-Signature: sha256=4f0c…e2a1

{
  "id": "7c1a0e2e-5b0b-4f0f-9a5e-2c6b6a1d3f10",
  "event": "deploy.succeeded",
  "created_at": "2026-09-10T09:14:02+00:00",
  "data": {
    "site": { "slug": "my-site", "name": "My site", "url": "https://example.com" },
    "ref": "main"
  }
}
```

`id` is unique per delivery and repeats on every retry of the same delivery, so use it to de-duplicate. `data` differs per event: site events carry `site`; incidents carry `incident` (id, title, severity, status, started_at, resolved_at) and `sites`; `domain.expiring` carries `domain`, `days_left`, `expires_at`, `auto_renew`; invoice events carry `invoice` (number, amount, currency, url).

Answer with any **2xx** status within 10 seconds. Do the real work afterwards — a receiver that does its processing before answering is the usual reason for timeouts and duplicate deliveries.

## Verifying the signature

When you add a webhook you are shown a secret once (`whsec_…`). Every delivery carries `X-Veldhost-Signature: sha256=<hex>`, the HMAC-SHA256 of the *raw* request body under that secret. Compute it yourself from the raw bytes — before any JSON parsing or re-encoding — and compare with a constant-time comparison. Reject anything that does not match.

**PHP**

```
$secret = getenv('VELDHOST_WEBHOOK_SECRET');
$raw = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $raw, $secret);

if (! hash_equals($expected, $_SERVER['HTTP_X_VELDHOST_SIGNATURE'] ?? '')) {
    http_response_code(401);
    exit;
}

$event = json_decode($raw, true);
// $event['event'], $event['data'] ...
http_response_code(204);
```

**Node.js** (Express — keep the raw body; a JSON body parser must not run first):

```
import crypto from 'node:crypto';
import express from 'express';

const app = express();
const secret = process.env.VELDHOST_WEBHOOK_SECRET;

app.post('/hooks/veldhost', express.raw({ type: 'application/json' }), (req, res) => {
  const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(req.body).digest('hex');
  const given = req.get('X-Veldhost-Signature') ?? '';

  if (expected.length !== given.length || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(given))) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(req.body.toString('utf8'));
  // event.event, event.data ...
  res.sendStatus(204);
});
```

Lost the secret, or think it leaked? Open the integration and choose **Roll signing secret** in its ⋯ menu. A new secret is generated and shown once, and every delivery from then on is signed with it; the old one stops signing at once. **Edit** on the same page changes the URL and the events without touching the secret or the log.

## Retries and pausing

A delivery that gets no 2xx — a timeout, a connection error, a 4xx or 5xx — is retried after one minute and again after ten, three attempts in all. Each attempt sends the same body and the same `X-Veldhost-Delivery` id. The delivery log on the Integrations page shows the response code and the first part of the response body for every delivery of the last 200.

From the delivery log you can **Resend** any settled delivery, or **Resend failed** to send the failed ones again (the newest 25 at a time). A resend is a new delivery with its own `id` and `X-Veldhost-Delivery`, the original `data`, and a fresh timestamp and signature. It also carries `X-Veldhost-Redelivery-Of` and, in the body, `redelivery_of`: the id of the delivery it repeats, so a receiver that already handled that one can skip it.

After **twenty deliveries in a row** fail their final attempt, the endpoint is paused and you get an email. Fix the receiver, then press *Enable*; nothing from the paused period is replayed. A single successful delivery resets the failure count.

## Slack

1. In Slack, open the channel's settings → *Integrations* → *Add an app* → **Incoming WebHooks** (or create a Slack app with an incoming webhook), and copy the URL that starts with `https://hooks.slack.com/`.
2. Under Integrations in veldhost Manage, choose kind **Slack**, paste the URL and pick your events.
3. Press *Send test event*. A message should appear in the channel within a few seconds.

Each event arrives as a short message with the site, what happened, and a few facts (a URL, how long an outage lasted, an invoice number). The signature headers are still sent; Slack ignores them.

## Microsoft Teams

1. In Teams, open the channel → *…* → *Workflows* → **Post to a channel when a webhook request is received** (or a classic Incoming Webhook connector where still available) and copy the URL.
2. Choose kind **Microsoft Teams**, paste the URL and pick your events.
3. Press *Send test event*.

Teams receives an Adaptive Card (a `message` with one `application/vnd.microsoft.card.adaptive` attachment), which both Workflows and the classic connector accept.

## Over the API

The same endpoints are on the customer API with the `manage` scope:

```
GET    /api/v1/webhooks                 list your endpoints and the subscribable events
POST   /api/v1/webhooks                 {"kind": "generic", "url": "https://…", "events": ["deploy.failed", "site.down"]}
                                        → 201 with the secret, once
DELETE /api/v1/webhooks/{id}            remove an endpoint
POST   /api/v1/webhooks/{id}/test       queue a webhook.test delivery
```

Everything you do here is recorded in your account's audit trail, like every other API write.
