A webhook is an address on your server where Jazui sends a POST with JSON when something happens. There are two per site: the blog one (articles published, updated or unpublished) and the alerts one (render errors, Googlebot drop, SEO score drop). Both are signed with the same site secret.
Setting them up
In the dashboard, open the site settings:
- Blog and IndexNow: blog webhook address. With Render the article page again (on by default), Jazui also refreshes the article page in the cache.
- Alerts: alerts webhook address (if your plan includes alert webhooks).
The address must be public, over https or http. Saving the first webhook generates the site's secret (32 characters), shown in the settings. With Test (blog) you get a ping event, and with Send test alert (alerts), an alert of type Test.
How they arrive
A POST with Content-Type: application/json and these headers:
X-Jazui-Event: the type (Published,Updated,Unpublished,alertorping).X-Jazui-Delivery: delivery ID, the same on retries (use it to avoid processing the same one twice).X-Jazui-Signature:sha256=followed by the HMAC-SHA256 of the body with your secret, in lowercase hex.
POST https://misitio.com/webhooks/jazui
Content-Type: application/json
X-Jazui-Event: Published
X-Jazui-Delivery: 3f0c9a3e-7a51-4f43-9c1e-0b8d2f6a4e11
X-Jazui-Signature: sha256=5d41402abc4b2a76b9719d911017c592ae8b6e1f0a2c4d3b5e7f9a1c3d5e7f90Blog events
event is article.published, article.updated or article.unpublished. The same article with the same event isn't sent twice within 30 seconds. Use it to rebuild your static site, notify your team or publish elsewhere.
{
"event": "article.published",
"site": "misitio.com",
"occurredAt": "2026-11-03T00:47:24Z",
"article": {
"id": "7c1d0e2f-…",
"slug": "como-funciona-el-prerender",
"url": "https://misitio.com/blog/como-funciona-el-prerender",
"language": "es",
"title": "Cómo funciona el prerender"
}
}Alerts
alert.type is RenderErrors (render errors in 24 h), GoogleDrop (Googlebot visits dropping versus the previous week), SeoDrop (SEO score dropping over 7 days) or Test. value is what was measured and threshold the threshold you set.
{
"event": "site.alert",
"site": "misitio.com",
"occurredAt": "2026-11-03T09:00:00Z",
"alert": {
"type": "SeoDrop",
"message": "La nota SEO media de misitio.com ha bajado 12 puntos en 7 días.",
"value": 12,
"threshold": 10,
"detail": "90 → 78"
}
}Test
The blog webhook test button sends:
{ "event": "ping", "site": "misitio.com", "occurredAt": "2026-11-03T09:00:00Z" }Verifying the signature
Compute the HMAC-SHA256 of the body exactly as received (without re-serializing it) with your secret and compare it with the header in constant time. If it doesn't match, answer 401 and don't process it.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
app.post('/webhooks/jazui', express.raw({ type: 'application/json' }), (req, res) => {
const expected = 'sha256=' + crypto.createHmac('sha256', process.env.JAZUI_WEBHOOK_SECRET)
.update(req.body).digest('hex');
const received = req.get('X-Jazui-Signature') || '';
if (expected.length !== received.length ||
!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString('utf8'));
// event.event: article.published, article.updated, article.unpublished, site.alert o ping
res.sendStatus(204);
});app.MapPost("/webhooks/jazui", async (HttpRequest request, IConfiguration config) =>
{
using var reader = new StreamReader(request.Body, Encoding.UTF8);
var body = await reader.ReadToEndAsync();
var key = Encoding.UTF8.GetBytes(config["Jazui:WebhookSecret"]!);
var expected = "sha256=" + Convert.ToHexString(HMACSHA256.HashData(key, Encoding.UTF8.GetBytes(body))).ToLowerInvariant();
var received = request.Headers["X-Jazui-Signature"].ToString();
if (!CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(received)))
return Results.Unauthorized();
using var json = JsonDocument.Parse(body);
var type = json.RootElement.GetProperty("event").GetString();
return Results.NoContent();
});Responses and retries
- Any response below
500counts as delivered: answer2xxas soon as you receive it and do the work afterwards. - With a
5xxor no response, Jazui retries after 2 and 10 seconds (three attempts in total). - Each attempt waits at most 15 seconds and follows up to 3 redirects.