Guide to Sevalla Webhooks: Features and Best Practices
Sevalla webhooks tell your systems what is happening on your hosting platform: an application or static site is created, updated or deleted, and above all, a deployment starts and finishes. If you deploy on Sevalla, webhooks let you post deploy notifications to chat, run tests after a deployment, or keep another system in sync without polling the Sevalla API.
This guide covers how Sevalla webhooks are created, the events and payload, what the svl-signature header really contains, how retries and secret rollover work, and the practices that keep a receiver reliable.
What are Sevalla webhooks?
A Sevalla webhook sends an HTTP POST with a JSON body to your endpoint each time one of the events you picked happens anywhere in your company. A company can have up to 10 webhooks, and each covers every application and static site, so you can't limit one to a single service. Filter on the IDs in the payload instead.
Each webhook has a signing secret that starts with whsec_, and each request has an svl-signature header. Despite those names, nothing is signed. Sevalla's docs are explicit: "The svl-signature header contains the webhook's signing secret itself, not a hash of the body." Your receiver compares the header with the secret it stored. Libraries for Standard Webhooks or HMAC signatures won't work here.
Sevalla webhook features
| Feature | Details |
|---|---|
| Configuration | Tools > Webhooks > Create webhook, or the API and CLI |
| Permissions | Company owner or Company admin, and a valid payment method |
| Limit | 10 webhooks per company |
| Scope | Every application and static site in the company |
| Method | POST, content-type: application/json |
| Authentication | svl-signature header carrying the secret (whsec_ plus 30 characters) |
| Signature | None. The header is the secret, not a hash |
| Success | Any 2xx |
| Retries | Up to 10, from 1 minute to 1 day apart, within 4 days of the event |
| Ordering | Not guaranteed |
| Secret rotation | Roll secret, with the old secret kept for up to 24 hours |
| Delivery log | Event deliveries tab, with request and response for each attempt |
Common events
| Event | Sent when |
|---|---|
APP_CREATE | An application is created |
APP_DEPLOY | A deployment starts (status: "inProgress"), and again when it ends (success, failed or cancelled) |
APP_UPDATE | An application's name, default branch, automatic deployments, hibernation or project is changed in the dashboard |
APP_DELETE | An application is deleted |
STATIC_SITE_CREATE, STATIC_SITE_UPDATE, STATIC_SITE_DELETE | A static site is created, has its settings updated, or is deleted |
STATIC_SITE_DEPLOY | A static site deployment starts, and again when it ends |
DATABASE_CREATE, DATABASE_UPDATE, DATABASE_DELETE | Not sent at the moment |
Two of these behave unexpectedly. APP_UPDATE only fires for changes made in the dashboard: "Changes made through the API don't send it." And the database events can be selected but aren't sent yet, so a webhook subscribed only to them receives nothing.
Every body has the same envelope. Here is Sevalla's APP_DEPLOY example:
{
"webhook_id": "d5f8a1c2-3b4e-4f6a-8b8c-9d0e1f2a3b4c",
"event_id": "a1b2c3d4-5e6f-4a8b-9c0d-1e2f3a4b5c6d",
"event_delivery_id": "f1e2d3c4-b5a6-4788-9a0b-c1d2e3f4a5b6",
"type": "APP_DEPLOY",
"data": {
"company_id": "0c9e3f5a-7b2d-4e1f-8a6c-5d4b3a2f1e0d",
"app_id": "7f3c2b1a-9e8d-4c7b-a6f5-4e3d2c1b0a9f",
"app_name": "storefront-x7k2m",
"app_display_name": "storefront",
"deployment_id": "3b2a1c0d-8e7f-4a6b-9c5d-2e1f0a9b8c7d",
"status": "success",
"commit": "9fceb02d0ae598e95dc970b74767f19372d61af8",
"stage": "",
"is_preview": false
}
}
event_id is the same on every retry, so use it to drop duplicates. event_delivery_id is new on every attempt. In deploy events, commit is absent or null for Docker image deployments, stage is the pipeline stage or an empty string, and is_preview is true for preview apps and pull request previews. In APP_UPDATE, name and display_name hold the values from before the update.
Setting up Sevalla webhooks
You need the Company owner or Company admin role, and the company needs a valid payment method.
- In the sidebar, under Tools, click Webhooks, then Create webhook.
- Enter your HTTPS endpoint in Endpoint, and optionally a Description. Endpoints that resolve to private, loopback or link-local addresses are rejected.
- Under Allowed events, tick the events to send.
- Click Create webhook. It starts sending events straight away.
- Click the webhook, and on its Overview tab under Details, copy the Signing secret. Store it server-side, for example as
SEVALLA_WEBHOOK_SECRET.
From the CLI, sevalla webhooks create --endpoint <url> --events APP_ does the same, and sevalla webhooks get <id> returns the secret.
With Hookdeck. Sevalla is a supported source in Hookdeck. Create a source with the Sevalla type, paste the whole secret into Signing Secret, including the whsec_ prefix, and use the Hookdeck source URL as the webhook's endpoint. Hookdeck compares svl-signature with that value. Hookdeck's setup note says that when you roll the secret, you update it on the source when the new one becomes active. The source accepts POST only, which is the method Sevalla uses. A request without the right header still gets a 200 from Hookdeck, but Hookdeck marks it as failed verification and doesn't deliver it to your service.
Securing Sevalla webhooks
Compare svl-signature with your stored secret in constant time, and during a rollover accept both the old and the new secret, as Sevalla recommends. Check lengths before calling crypto.timingSafeEqual.
const crypto = require('crypto');
const express = require('express');
// The current secret, and during a rollover the old one too.
const SECRETS = [process.env.SEVALLA_WEBHOOK_SECRET, process.env.SEVALLA_WEBHOOK_SECRET_OLD].filter(Boolean);
function isFromSevalla(header) {
if (typeof header !== 'string' || SECRETS.length === 0) return false;
const received = Buffer.from(header, 'utf8');
return SECRETS.some((secret) => {
const expected = Buffer.from(secret, 'utf8');
return expected.length === received.length && crypto.timingSafeEqual(expected, received);
});
}
const app = express();
app.post('/webhooks/sevalla', express.raw({ type: '*/*' }), (req, res) => {
if (!isFromSevalla(req.get('svl-signature'))) {
return res.sendStatus(401);
}
let event;
try {
event = JSON.parse(req.body.toString('utf8'));
} catch {
return res.sendStatus(400);
}
res.sendStatus(200); // answer first, then do the work
// Deduplicate on event.event_id, then route on event.type.
});
The same check in Python:
import hmac
import os
SECRETS = [
s
for s in (os.environ.get("SEVALLA_WEBHOOK_SECRET"), os.environ.get("SEVALLA_WEBHOOK_SECRET_OLD"))
if s
]
def is_from_sevalla(header) -> bool:
if not header:
return False
received = header.encode("utf-8")
return any(hmac.compare_digest(received, s.encode("utf-8")) for s in SECRETS)
Because the header is the secret itself, it proves the sender knows the secret but not that the body is unchanged, and it gives no replay protection: nothing is signed and there is no timestamp. Anyone who sees one request can reuse the secret. Use an https:// endpoint, as Sevalla advises "because the signing secret travels in a header", keep the secret out of logs, and roll it if it might have leaked.
Make Sevalla webhooks production-ready. Hookdeck Event Gateway checks
svl-signatureon every Sevalla request, stores each deploy and application event, and lets you replay any of them, which Sevalla's dashboard can't.
Sevalla webhook limitations and pain points
A "signature" that isn't one
The Problem: HMAC or Standard Webhooks verification fails on every request.
Why It Happens: The whsec_ prefix and the svl-signature name suggest a signature scheme, but the header carries the secret unchanged.
Workarounds:
- Compare the header with the secret as a plain string, in constant time.
- Don't decode, strip or hash the
whsec_value.
How Hookdeck Can Help: Hookdeck's Sevalla source compares the header with the whole secret you paste, so you don't have to reimplement the check in every service.
Rolling the secret has a cut-over moment
The Problem: After a rollover, requests start failing verification.
Why It Happens: Roll secret lets you keep the old secret on deliveries for Now, 1, 2, 4, 8, 16 or 24 hours (the default). "Deliveries keep carrying the old secret until that time, then the new one." Retries carry the current secret too, so a retry of an older event switches at the same moment.
Workarounds:
- Add the new secret to your receiver as soon as you roll, and keep the old one until the period ends, as in the code above.
- Avoid Now unless your receiver already has the new secret.
How Hookdeck Can Help: A Hookdeck source holds one value, so update Signing Secret at the moment the new secret becomes active, which the webhook's Rolling over note shows. Because Hookdeck answers 200 even to requests that fail verification, Sevalla won't retry those, so keep the gap short and check Hookdeck's request log afterwards.
Retries stop after 4 days, and there is no resend button
The Problem: An event missed during a long outage is gone.
Why It Happens: Sevalla retries a failed event up to 10 times, at 1 minute, 5 minutes, 10 minutes, 30 minutes, 1, 2, 4, 8 and 16 hours, then 1 day, and only within 4 days of the event. "There's no button to resend an event by hand." Events that happen while a webhook is disabled aren't sent later either.
Workarounds:
- Answer
2xxquickly so slow work doesn't count as a failure. - After an outage, read deployment status from the Sevalla API.
How Hookdeck Can Help: Hookdeck accepts each delivery as it arrives and stores it, then delivers to your service with automatic retries and lets you replay any stored event. Issues alert you when deliveries fail.
Every service, every event
The Problem: A webhook meant for one application receives events from all of them.
Why It Happens: Webhooks are company-wide, and there are only 10 per company.
Workarounds:
- Filter on
app_idorstatic_site_idin your handler.
How Hookdeck Can Help: Hookdeck filters can route on data.app_id, type or data.status, so one Sevalla webhook can feed several services, each seeing only its own events.
Best practices
Handle both deploy events
APP_DEPLOY and STATIC_SITE_DEPLOY fire at the start and the end of a deployment. Check data.status before announcing a successful release, and expect inProgress first.
Deduplicate on event_id
Each retry has a new event_delivery_id but the same event_id. Store event_id before processing and skip repeats. See our guide to webhook idempotency.
Don't rely on order
Sevalla says ordering isn't guaranteed. A success can arrive before its inProgress, so keep the latest state per deployment_id rather than applying events in arrival order.
Acknowledge fast and process asynchronously
Sevalla's advice is to respond with a 2xx as soon as you've checked the header and "do slow work afterwards". See why to process webhooks asynchronously.
Conclusion
Sevalla sends JSON POST requests for application and static site events, with the webhook's whsec_ secret copied into svl-signature. Compare that header as a plain string in constant time over HTTPS, accept both secrets during a rollover, deduplicate on event_id, and handle both the start and end of each deployment.
Hookdeck Event Gateway checks svl-signature, stores every deploy event, and retries delivery to your service on your own schedule, with replay when you need it.
Get started with Hookdeck for free and handle Sevalla webhooks reliably in minutes.