Webhooks
A webhook makes LVNG send an HTTP POST to your server when something happens in your workspace. Workspace owners and admins add them in Settings > Developer > Webhooks, or with POST /api/v2/developer/webhooks.
Events LVNG sends
| Event | When |
|---|---|
message.created | A message was posted in a channel that every member of the workspace can read. Messages in private channels and direct messages are not sent. |
workflow.completed | A workflow run finished successfully, whether it was started from the app, through the API or by a schedule. |
Accepted, but not sent yet: agent.completed, credit.low, swarm.spawn, knowledge.indexed. You can subscribe to these events, but LVNG does not send them today. Any other event name is refused with 400.
The Test button (or POST /api/v2/developer/webhooks/:id/test) sends one test.ping event with empty data.
Payload Format
Every delivery is a JSON body with the event name, a timestamp and a data object. Ids are plain UUIDs. A workflow delivery carries the run's ids and timing, not its inputs or outputs.
{
"event": "message.created",
"timestamp": "2026-10-13T10:30:00.512Z",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"workspace_id": "a1b2c3d4-e5f6-4890-abcd-ef1234567890",
"channel_id": "660e8400-e29b-41d4-a716-446655440000",
"channel_name": "general",
"parent_message_id": null,
"sender_id": "770e8400-e29b-41d4-a716-446655440000",
"ai_twin_id": null,
"content": "Hello from LVNG!",
"content_type": "text",
"attachment_count": 0,
"created_at": "2026-10-13T10:30:00.000Z"
}
}{
"event": "workflow.completed",
"timestamp": "2026-10-13T10:31:12.004Z",
"data": {
"workspace_id": "a1b2c3d4-e5f6-4890-abcd-ef1234567890",
"run_id": "880e8400-e29b-41d4-a716-446655440000",
"workflow_id": "990e8400-e29b-41d4-a716-446655440000",
"workflow_name": "Weekly report",
"status": "completed",
"started_at": "2026-10-13T10:30:41.000Z",
"finished_at": "2026-10-13T10:31:11.000Z",
"duration_ms": 30000
}
}Verifying Signatures
Each request carries an X-LVNG-Signature header: the hex HMAC-SHA256 of the raw request body, keyed with the webhook's signing secret. The secret is shown once, when you create the webhook. The X-LVNG-Event header names the event.
const crypto = require('crypto');
const express = require('express');
const app = express();
// Sign-check the raw bytes LVNG sent, before parsing them.
app.post('/webhooks/lvng', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.get('x-lvng-signature') || '';
const expected = crypto
.createHmac('sha256', process.env.LVNG_WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
const valid = signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!valid) {
return res.status(401).json({ error: 'Invalid signature' });
}
const { event, data } = JSON.parse(req.body.toString('utf8'));
console.log('Received event:', event, data);
res.status(200).json({ received: true });
});Delivery
Respond within 10 seconds
LVNG waits 10 seconds for an answer. Return a 2xx status, then do slow work after you answer.
One attempt, no retries
Each event is sent once. Every attempt, with its status code and time taken, is listed by GET /api/v2/developer/webhooks/:id/deliveries.
Verify signatures
Always check the X-LVNG-Signature header before acting on an event.
Use a public HTTPS address
The address must be a public http or https URL; private and internal addresses are refused. Use HTTPS so the content is encrypted on the way.