Webhook Integration
The Vev webhook is the recommended approach for enterprise-level self-hosting. Instead of using embed scripts or ZIP downloads, the webhook delivers your project's HTML, CSS, and JavaScript directly to your server on every publish. This gives you full control over rendering, hosting, and caching — resulting in the best possible performance for SEO, LLMs, and page load times.
Why use the webhook?
| Approach | SEO | Load time | Self-hosted | Automation |
|---|---|---|---|---|
| Webhook | Server-rendered HTML | Fastest | Full control | Automatic on publish |
| Embed script | Client-rendered | Slower (extra JS) | Partial | Manual |
| ZIP download | Depends | Depends | Full control | Manual |
The webhook sends a POST request to your endpoint every time a project is published. Your server receives the complete page content and can serve it directly — no client-side rendering required. Search engines, LLMs, and performance tools see fully rendered HTML on first load.
How it works
- Configure a webhook destination in the Vev editor (Publishing > Add destination > Webhook)
- Publish your project from the editor
- Vev sends a
POSTrequest to your endpoint with the full page content - Your server stores the HTML and serves it to visitors
Vev Editor → Publish → POST to your endpoint → Your server stores & serves contentSetting up the webhook
In the Vev editor, go to Publishing and add a new Webhook destination. You will need to configure:
- Webhook URL — Your endpoint that accepts
POSTrequests (must be publicly accessible) - Authentication — Optional security for your endpoint (Basic Auth, Bearer Token, or none)
Webhook options
| Option | Description |
|---|---|
| Send full page | When enabled, the payload includes complete HTML documents with <html>, <head>, and <body> tags, styles, and scripts. When disabled, only the body content is sent — useful when you want to inject Vev content into your own page template. |
| Send plugin settings | Include configuration data for any Vev plugins/integrations used in the project. |
| Content as links | Instead of including HTML inline in the payload, Vev provides temporary download URLs. Useful for very large pages. |
| Include asset URLs | All asset URLs (images, fonts, etc.) are listed in the payload with paths rewritten for local hosting. This is the key option for full self-hosting. |
| Custom assets folder | Set the folder name for locally hosted assets (defaults to assets). |
| Fix og path | Converts relative og:image meta tag paths to absolute URLs for proper social media sharing. |
Payload structure
Every webhook request is a JSON POST with this structure:
{
"id": "unique-event-id",
"hosting": "hosting-key",
"event": "PUBLISH",
"payload": {
"projectId": "abc123",
"projectTitle": "My Project",
"version": 42,
"dir": "/my-project",
"pages": [...],
"css": [...],
"js": [...],
"other": [...]
}
}Events
The webhook sends three types of events:
| Event | Description |
|---|---|
| PUBLISH string | Sent when a project is published. Contains the full page payload. |
| UNPUBLISH string | Sent when a project is unpublished. Contains only the project ID. |
| PING string | Sent to test connectivity when configuring the webhook. |
Pages
Each page in the pages array contains:
{
"key": "page-key",
"title": "Page Title",
"path": "/about",
"html": "<div>...full page content...</div>",
"index": true
}When Send full page is enabled, the html field contains a complete HTML document:
<!DOCTYPE html>
<html>
<head>
<title>Page Title</title>
<link rel="stylesheet" href="https://cdn.vev.design/..." />
<!-- meta tags, og tags, etc. -->
</head>
<body>
<div>...page content...</div>
<script src="https://cdn.vev.design/..."></script>
</body>
</html>When disabled, only the inner body content is provided — giving you full control over the surrounding HTML structure.
When Content as links is enabled, the html field is replaced with a downloadUrl pointing to a temporary storage location where the HTML can be fetched.
CSS and JavaScript
External stylesheets and scripts are listed separately:
{
"css": [
{ "url": "https://cdn.vev.design/...", "contentType": "text/css" }
],
"js": [
{ "url": "https://cdn.vev.design/...", "contentType": "application/javascript" }
]
}Assets (self-hosting mode)
When Include asset URLs is enabled, the payload includes:
assets— An array of URLs for all images, fonts, and other assets used by the projectassetsFolder— The folder name assets are mapped to (default:assets)embedScripts— Local embed script paths with download URLs- All asset references in the HTML and CSS are rewritten to use relative paths pointing to the assets folder
{
"assets": [
"https://storage.googleapis.com/.../assets/image1.webp",
"https://storage.googleapis.com/.../assets/style.css"
],
"assetsFolder": "assets",
"embedScripts": [
{ "downloadUrl": "https://storage.googleapis.com/.../embed.js", "localPath": "embed.js" }
]
}Implementation guide
Option 1: Full page with Vev CDN (simplest)
Use the HTML as a complete page and let Vev's CDN (powered by Cloudflare) serve the CSS, JS, and assets. Best for getting started quickly.
Webhook settings:- Send full page: enabled
- Include asset URLs: disabled
// Express.js example
app.post('/vev-webhook', express.json({ limit: '50mb' }), (req, res) => {
const { event, payload } = req.body;
if (event === 'PUBLISH') {
for (const page of payload.pages) {
// Store the full HTML — assets load from Vev CDN
const filePath = page.index ? 'index.html' : `${page.path}.html`;
fs.writeFileSync(`./public/${filePath}`, page.html);
}
}
if (event === 'UNPUBLISH') {
// Remove published files
}
res.json({ received: true });
});Option 2: Body content injected into your template
Receive only the body content and inject it into your existing page layout. This is ideal when Vev content is part of a larger page with your own header, footer, and navigation.
Webhook settings:- Send full page: disabled
- Include asset URLs: disabled
app.post('/vev-webhook', express.json({ limit: '50mb' }), (req, res) => {
const { event, payload } = req.body;
if (event === 'PUBLISH') {
for (const page of payload.pages) {
// Store body content to be injected into your template at render time
db.upsert('vev_pages', {
path: page.path,
html: page.html,
title: page.title,
projectId: payload.projectId,
version: payload.version,
});
}
// Store CSS and JS references
db.upsert('vev_assets', {
projectId: payload.projectId,
css: payload.css.map(f => f.url),
js: payload.js.map(f => f.url),
});
}
res.json({ received: true });
});
// Serve the page with your own template
app.get('/landing/:slug', (req, res) => {
const page = db.get('vev_pages', { path: `/${req.params.slug}` });
const assets = db.get('vev_assets', { projectId: page.projectId });
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>${page.title} — My Site</title>
${assets.css.map(url => `<link rel="stylesheet" href="${url}" />`).join('\n')}
<!-- Your own styles -->
<link rel="stylesheet" href="/styles/main.css" />
</head>
<body>
<nav><!-- Your navigation --></nav>
<main>${page.html}</main>
<footer><!-- Your footer --></footer>
${assets.js.map(url => `<script src="${url}"></script>`).join('\n')}
</body>
</html>
`);
});Option 3: Fully self-hosted (recommended for enterprise)
Download all assets and serve everything from your own infrastructure. This gives you complete control, independence from any external CDN, and the best performance through your own caching strategy.
Webhook settings:- Send full page: enabled (or disabled if injecting into a template)
- Include asset URLs: enabled
- Custom assets folder: set to your preferred path (e.g.,
static/vev)
const fetch = require('node-fetch');
const path = require('path');
app.post('/vev-webhook', express.json({ limit: '50mb' }), async (req, res) => {
// Respond quickly — process in background
res.json({ received: true });
const { event, payload } = req.body;
if (event !== 'PUBLISH') return;
const assetsDir = `./public/${payload.assetsFolder || 'assets'}`;
fs.mkdirSync(assetsDir, { recursive: true });
// 1. Download all assets to your server
for (const assetUrl of payload.assets || []) {
const fileName = path.basename(new URL(assetUrl).pathname);
const response = await fetch(assetUrl);
const buffer = await response.buffer();
fs.writeFileSync(path.join(assetsDir, fileName), buffer);
}
// 2. Download embed scripts
for (const script of payload.embedScripts || []) {
const response = await fetch(script.downloadUrl);
const content = await response.text();
fs.writeFileSync(`./public/${script.localPath}`, content);
}
// 3. Store HTML pages (asset paths are already rewritten to local paths)
for (const page of payload.pages) {
const html = page.html || await fetch(page.downloadUrl).then(r => r.text());
const filePath = page.index ? 'index.html' : `${page.path}.html`;
fs.writeFileSync(`./public/${filePath}`, html);
}
});In this mode, Vev rewrites all asset URLs in the HTML and CSS to point to your local assets folder. No requests go to Vev's CDN at runtime.
Verifying webhook requests
Every webhook request includes an X-Vev-Signature header containing an HMAC-SHA512 signature of the request body, signed with your webhook secret.
const crypto = require('crypto');
function verifySignature(req, secret) {
const signature = req.headers['x-vev-signature'];
if (!signature) return false;
const hmac = crypto.createHmac('sha512', secret);
const expected = 'sha512=' + hmac.update(JSON.stringify(req.body)).digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
app.post('/vev-webhook', express.json({ limit: '50mb' }), (req, res) => {
if (!verifySignature(req, process.env.VEV_WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process webhook...
});The request also includes an X-Vev-Hosting header with the hosting key, which can be used to identify the source destination.
Headers reference
| Header | Description |
|---|---|
| Content-Type string | Always application/json |
| X-Vev-Hosting string | The hosting key identifying the webhook destination |
| X-Vev-Signature string | HMAC-SHA512 signature of the request body (when a secret is configured) |
| Authorization string | Basic or Bearer token (when authentication is configured) |
Best practices
- Respond quickly — Return a
2xxresponse as fast as possible. Process heavy work (downloading assets, writing files) asynchronously after responding. - Handle idempotency — Use the
versionfield to avoid processing duplicate publishes. Store the latest version and skip older ones. - Set cache headers — When self-hosting assets, configure appropriate
Cache-Controlheaders. Vev assets are immutable per version, so long cache lifetimes are safe. - Use full page for SEO — The full page mode includes all meta tags, Open Graph tags, and structured data configured in the editor. This gives search engines and LLMs the best possible content to index.
- Monitor your endpoint — The Vev editor shows the HTTP response status from your webhook. Use this to debug integration issues.
- Size limits — For projects with many pages or large assets, enable Content as links to keep the payload size manageable. Your server then downloads the content from temporary URLs.