Notifications via webhook
A Node.js recipe — a small server receives Slicer webhooks, verifies the signature and sends a Telegram message when a video is ready or published.
On this page
A montage takes a few minutes, and there's no need to wait for it in the terminal. This recipe runs a small server that receives Slicer webhooks and writes to Telegram: "Video ready" with a link to the project, "Published" with a link to the video on the social network. Sending messages to Slack, email or your task tracker works the same way.
You will need: Node.js 20+, a server with a public https address (or a tunnel for testing), a token with the
webhooks scope, a Telegram bot (its token comes from @BotFather (opens in a new tab)) and the id of the chat to
write to.
The server
// npm install express
// SLICER_WEBHOOK_SECRET=whsec_… TELEGRAM_BOT_TOKEN=… TELEGRAM_CHAT_ID=… node notify.mjs
import { createHmac, timingSafeEqual } from 'node:crypto';
import express from 'express';
const TOLERANCE_SEC = 5 * 60;
function verifySlicerSignature(rawBody, header, secret) {
if (typeof header !== 'string') return false;
const pairs = header.split(',').map((part) => part.trim().split('='));
const timestamp = pairs.find(([key]) => key === 't')?.[1];
const signatures = pairs.filter(([key]) => key === 'v1').map(([, value]) => value);
if (!timestamp || !/^\d+$/.test(timestamp) || signatures.length === 0) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_SEC) return false;
const expected = createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest();
return signatures.some((value) => {
const received = Buffer.from(value, 'hex');
return received.length === expected.length && timingSafeEqual(received, expected);
});
}
async function telegram(text) {
const response = await fetch(`https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendMessage`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chat_id: process.env.TELEGRAM_CHAT_ID, text, disable_web_page_preview: true }),
});
if (!response.ok) console.error('Telegram responded', response.status);
}
function message(event) {
const item = event.data ?? {};
switch (event.type) {
case 'project.succeeded':
return `🎬 "${item.title}" is ready — ${Math.round(item.durationSec ?? 0)} s.\n${item.appUrl}`;
case 'export.succeeded':
return `📦 The export of "${item.title}" is ready.\n${item.appUrl}`;
case 'publication.published':
return `🚀 Published to ${item.platform}: ${item.url}`;
case 'ping':
return '✅ Slicer webhook connected';
default:
return null; // this bot doesn't need other events
}
}
const app = express();
const seen = new Set(); // event ids: a repeated delivery won't send a second message
app.post('/slicer/webhook', express.raw({ type: 'application/json' }), (req, res) => {
if (!verifySlicerSignature(req.body, req.get('Slicer-Signature'), process.env.SLICER_WEBHOOK_SECRET)) {
return res.status(400).send('invalid signature');
}
const event = JSON.parse(req.body.toString('utf8'));
res.sendStatus(204); // Slicer waits no longer than 10 seconds — answer right away
if (seen.has(event.id)) return;
seen.add(event.id);
const text = message(event);
if (text) telegram(text).catch((error) => console.error(error));
});
app.listen(Number(process.env.PORT ?? 3000), () => console.log('Waiting for Slicer webhooks'));Setup
Run the server over https
Host
notify.mjson a server with a publichttpsaddress. For testing on your own computer, a tunnel works:Terminal node notify.mjs & cloudflared tunnel --url http://localhost:3000The tunnel prints an address like
https://….trycloudflare.com.Register the webhook
Terminal curl https://api.slicer.media/v1/webhooks \ -H "Authorization: Bearer $SLICER_TOKEN" \ -H "Content-Type: application/json" \ -d '{"url":"https://notify.example.com/slicer/webhook","events":["project.succeeded","export.succeeded","publication.published"],"description":"Telegram"}' \ | jq -r '.data.secret, .data.webhook.id'The first line of the output is the
whsec_…secret: pass it to the server asSLICER_WEBHOOK_SECRETand restart it. The second is the webhook id.Test it
Terminal curl -X POST https://api.slicer.media/v1/webhooks/$WEBHOOK_ID/test -H "Authorization: Bearer $SLICER_TOKEN"The chat gets "✅ Slicer webhook connected". If not, check the delivery log:
GET /v1/webhooks/{id}/deliveriesshows your server's response code.
Fine points
- Raw body.
express.rawhands over the body as is — the signature is computed over exactly that. If your app already usesexpress.json()globally, register the webhook route before it. - Retries. If the server didn't answer within 10 seconds or answered with an error, Slicer delivers again later. The
seenset prevents a second message; in production keep event ids in Redis or a database. - Security. Don't log the secret and don't reply with details to a bad signature. The
appUrllink opens the project only after signing in to Slicer, so it's fine to send it to a team chat.