Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Webhook Integration – Vev Developer
Skip to content

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?

ApproachSEOLoad timeSelf-hostedAutomation
WebhookServer-rendered HTMLFastestFull controlAutomatic on publish
Embed scriptClient-renderedSlower (extra JS)PartialManual
ZIP downloadDependsDependsFull controlManual

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

  1. Configure a webhook destination in the Vev editor (Publishing > Add destination > Webhook)
  2. Publish your project from the editor
  3. Vev sends a POST request to your endpoint with the full page content
  4. Your server stores the HTML and serves it to visitors
Vev Editor → Publish → POST to your endpoint → Your server stores & serves content

Setting 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 POST requests (must be publicly accessible)
  • Authentication — Optional security for your endpoint (Basic Auth, Bearer Token, or none)

Webhook options

OptionDescription
Send full pageWhen 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 settingsInclude configuration data for any Vev plugins/integrations used in the project.
Content as linksInstead of including HTML inline in the payload, Vev provides temporary download URLs. Useful for very large pages.
Include asset URLsAll 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 folderSet 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:

EventDescription
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 project
  • assetsFolder — 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

HeaderDescription
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 2xx response as fast as possible. Process heavy work (downloading assets, writing files) asynchronously after responding.
  • Handle idempotency — Use the version field to avoid processing duplicate publishes. Store the latest version and skip older ones.
  • Set cache headers — When self-hosting assets, configure appropriate Cache-Control headers. 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.