Gareth Wilson Gareth Wilson

Guide to Sevalla Webhooks: Features and Best Practices

Published


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

FeatureDetails
ConfigurationTools > Webhooks > Create webhook, or the API and CLI
PermissionsCompany owner or Company admin, and a valid payment method
Limit10 webhooks per company
ScopeEvery application and static site in the company
MethodPOST, content-type: application/json
Authenticationsvl-signature header carrying the secret (whsec_ plus 30 characters)
SignatureNone. The header is the secret, not a hash
SuccessAny 2xx
RetriesUp to 10, from 1 minute to 1 day apart, within 4 days of the event
OrderingNot guaranteed
Secret rotationRoll secret, with the old secret kept for up to 24 hours
Delivery logEvent deliveries tab, with request and response for each attempt

Common events

EventSent when
APP_CREATEAn application is created
APP_DEPLOYA deployment starts (status: "inProgress"), and again when it ends (success, failed or cancelled)
APP_UPDATEAn application's name, default branch, automatic deployments, hibernation or project is changed in the dashboard
APP_DELETEAn application is deleted
STATIC_SITE_CREATE, STATIC_SITE_UPDATE, STATIC_SITE_DELETEA static site is created, has its settings updated, or is deleted
STATIC_SITE_DEPLOYA static site deployment starts, and again when it ends
DATABASE_CREATE, DATABASE_UPDATE, DATABASE_DELETENot 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.

  1. In the sidebar, under Tools, click Webhooks, then Create webhook.
  2. Enter your HTTPS endpoint in Endpoint, and optionally a Description. Endpoints that resolve to private, loopback or link-local addresses are rejected.
  3. Under Allowed events, tick the events to send.
  4. Click Create webhook. It starts sending events straight away.
  5. 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_DEPLOY,STATIC_SITE_DEPLOY 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-signature on 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 2xx quickly 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_id or static_site_id in 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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.