VellumUp
PricingBlogIntegrations
Sign inRegister
Back to Docs

On this page

All Integrations

WordpressShopifyWebflowWixWebhooks
Back to Docs

Events · HTTP POST

Webhooks

Instant article delivery to your server — no polling, no API keys.

How it works

Instant article delivery to your server — no polling, no API keys.

1

Register your HTTPS endpoint URL below. VellumUp will POST to it whenever an event fires.

2

Each delivery is signed with HMAC-SHA256. Verify the X-VellumUp-Signature header to confirm it came from us.

3

Parse the JSON payload, save the article to your database, trigger a rebuild, or do anything you like.

Events

Your endpoint receives one of these events on every delivery — read X-VellumUp-Event header to route accordingly

article.published

Fired when a new article is generated, or when the article status is changed to published

article.unpublished

Fired when an article is moved back to draft status

article.updated

Fired when an article's title, content, keywords, or cover image is edited and saved

article.translated

Fired each time a language translation is saved — includes language_code and language_name fields in the data object

Payload Reference

Every delivery contains the full article in its current state. All events share the same fields — only article.translated adds language_code and language_name.

Use the slug field to build the article URL on your site. It is URL-safe, unique per domain, and stable across updates.
FieldTypeDescription
idstring (UUID)Unique article ID in your account
slugstringURL-friendly identifier — build your article URL from this (e.g. yourblog.com/blog/slug)
titlestringFull article title
contentstringFull article body. Markdown by default — or HTML if you selected HTML format when creating the endpoint
cover_imagestring | nullAI-generated cover image URL, or null if image generation was skipped
cover_image_urlstring | nullSame as cover_image (explicit naming for clarity)
meta_descriptionstring | nullSEO meta description, up to 160 characters
focus_keywordstring | nullPrimary SEO keyword the article targets
secondary_keywordsstring[]Supporting keywords included in the content
faq{ question, answer }[]Array of question/answer objects — ready to render as an FAQ section
word_countnumberApproximate word count of the content
reading_time_minutesnumberEstimated reading time in minutes (word_count ÷ 200)
website_urlstring | nullFull URL of the connected website
website_domainstringDomain of the connected website (e.g. yourblog.com)
status"published" | "draft"Current article status
og_titlestringArticle title for social sharing & OG tags
og_descriptionstringMeta description (fallback to first 155 chars)
og_type"article"Content type for Open Graph metadata
created_atstring (ISO 8601)When the article was first generated
updated_atstring (ISO 8601)When the article was last modified
language_codestring (BCP-47)Only on article.translated — BCP-47 code e.g. "fr", "es", "de", "ar"
language_namestringOnly on article.translated — human-readable name e.g. "French"

Example payload

json
{
  "id":         "del_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "created_at": "2026-04-22T10:30:00.000Z",

  "data": {
    "id":                  "f7e6d5c4-b3a2-1098-fedc-ba9876543210",
    "slug":                "how-to-improve-your-seo-in-2026",
    "title":               "How to Improve Your SEO in 2026",
    "content":             "# How to Improve Your SEO in 2026\n\nSearch engine optimization has evolved...",
    "cover_image":         "https://storage.supabase.co/article-images/user123/ai/hero_abc123.jpg",
    "cover_image_url":     "https://storage.supabase.co/article-images/user123/ai/hero_abc123.jpg",
    "meta_description":    "Learn proven SEO strategies for 2026. From technical optimization to content strategy.",
    "focus_keyword":       "improve SEO 2026",
    "secondary_keywords":  ["on-page SEO", "SEO tips", "search ranking", "technical SEO"],
    "faq": [
      {
        "question": "How long does SEO take to show results?",
        "answer": "Typically 3–6 months for competitive keywords."
      },
      {
        "question": "What is the most important SEO factor?",
        "answer": "High-quality, relevant content remains the top ranking factor."
      }
    ],
    "word_count":          1840,
    "reading_time_minutes": 9,
    "website_url":         "https://yourblog.com",
    "website_domain":      "yourblog.com",
    "status":              "published",
    "og_title":            "How to Improve Your SEO in 2026",
    "og_description":      "Learn proven SEO strategies for 2026. From technical optimization to content strategy.",
    "og_type":             "article",
    "created_at":          "2026-04-22T10:30:00.000Z",
    "updated_at":          "2026-04-22T10:30:00.000Z"
  }
}

Verifying Webhook Requests

How signing works

Each delivery includes an X-VellumUp-Signature header formatted as t=TIMESTAMP,v1=SHA256_HEX. To verify: concatenate the timestamp and the raw request body as "TIMESTAMP.RAW_BODY", compute HMAC-SHA256 using your signing secret, and compare with v1 using a constant-time function. Always use the raw body — not parsed JSON — for the comparison.

HMAC-SHA256(whsec_secret, "<timestamp>.<raw_json_body>")// X-VellumUp-Signature: t=<ts>,v1=<hex>

Verify signature — code examples

const crypto = require('crypto');

const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET; // whsec_...

function verifySignature(rawBody, sigHeader, secret) {
  if (!sigHeader) return false;

  // Parse "t=<timestamp>,v1=<hmac>"
  const parts = Object.fromEntries(sigHeader.split(',').map(p => p.split('=')));
  const { t: ts, v1: received } = parts;
  if (!ts || !received) return false;

  // Reject requests older than 5 minutes
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${ts}.${rawBody}`)
    .digest('hex');

  return crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
}

Handling Webhook Payloads

Complete handlers — verify the signature, respond 200 immediately, then process the article data in the background.

// app/api/webhook/route.ts
import { NextRequest } from 'next/server';
import { createHmac, timingSafeEqual } from 'crypto';

const SECRET = process.env.WEBHOOK_SECRET!;

function verify(rawBody: string, sig: string): boolean {
  const parts = Object.fromEntries(sig.split(',').map(p => p.split('=')));
  const { t: ts, v1 } = parts;
  if (!ts || !v1) return false;
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const expected = createHmac('sha256', SECRET)
    .update(`${ts}.${rawBody}`)
    .digest('hex');
  try {
    return timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
  } catch { return false; }
}

export async function POST(req: NextRequest) {
  const rawBody = await req.text();
  const sig     = req.headers.get('x-vellumup-signature') ?? '';
  const event   = req.headers.get('x-vellumup-event') ?? '';

  if (!verify(rawBody, sig)) {
    return new Response('Unauthorized', { status: 401 });
  }

  const { data } = JSON.parse(rawBody);

  if (event === 'article.published') {
    // Save to your database
    // await db.articles.upsert({ where: { slug: data.slug }, ... })
    console.log('New article:', data.title, '→', data.slug);
  }
  if (event === 'article.translated') {
    // Save translation
    console.log(`[${data.language_code}] ${data.title}`);
  }
  if (event === 'article.updated') {
    // Update existing article
    console.log('Updated:', data.title);
  }

  return new Response('OK', { status: 200 });
}

Use Cases

Sync to Database or CMS

Save incoming articles directly to your database or headless CMS the moment they are generated — no manual export needed.

javascript
// Upsert article into your database on publish
if (event === 'article.published') {
  await db.articles.upsert({
    where:  { slug: data.slug },
    update: { title: data.title, content: data.content, updatedAt: new Date() },
    create: { slug: data.slug, title: data.title, content: data.content },
  });
}

Trigger a Site Rebuild

Call your hosting provider's deploy hook (Vercel, Netlify, etc.) to rebuild your static site whenever a new article arrives.

javascript
// Trigger a Vercel rebuild after publish
if (event === 'article.published') {
  await fetch(process.env.VERCEL_DEPLOY_HOOK_URL, { method: 'POST' });
}

// Or for Netlify:
if (event === 'article.published') {
  await fetch(process.env.NETLIFY_BUILD_HOOK, { method: 'POST' });
}

Multi-language Content Sync

Listen for article.translated events to automatically sync translated versions to the correct locale pages on your site.

javascript
// Save translations to the correct locale path
if (event === 'article.translated') {
  await db.translations.upsert({
    where:  { slug_locale: { slug: data.slug, locale: data.language_code } },
    update: { title: data.title, content: data.content },
    create: { slug: data.slug, locale: data.language_code,
              title: data.title, content: data.content },
  });
}

Send Notifications

Post to Slack, send an email, or push a notification to your team whenever a new article is published or updated.

javascript
// Post a Slack notification when a new article is published
if (event === 'article.published') {
  await fetch(process.env.SLACK_WEBHOOK_URL, {
    method:  'POST',
    headers: { 'Content-Type': 'application/json' },
    body:    JSON.stringify({
      text: `📝 New article published: *${data.title}*
${data.website_url}/${data.slug}`,
    }),
  });
}

Rendering Articles in React

VellumUp delivers article content as Markdown. Use the react-markdown package to render it in your React app — it handles headings, links, code blocks, and lists automatically.
typescript
// 1. Install react-markdown
// npm install react-markdown

// 2. components/Article.tsx
import ReactMarkdown from 'react-markdown';

interface Article {
  title:        string;
  content:      string;  // raw Markdown from webhook
  slug:         string;
  cover_image?: string | null;
}

export function Article({ article }: { article: Article }) {
  return (
    <article>
      {article.cover_image && (
        <img src={article.cover_image} alt={article.title} />
      )}
      <h1>{article.title}</h1>
      <div className="prose">
        <ReactMarkdown>{article.content}</ReactMarkdown>
      </div>
    </article>
  );
}

// 3. With syntax highlighting (optional)
// npm install react-markdown remark-gfm rehype-highlight

import ReactMarkdown    from 'react-markdown';
import remarkGfm        from 'remark-gfm';
import rehypeHighlight  from 'rehype-highlight';

<ReactMarkdown
  remarkPlugins={[remarkGfm]}
  rehypePlugins={[rehypeHighlight]}
>
  {article.content}
</ReactMarkdown>

Troubleshooting

Signature verification fails

Make sure you are using the raw request body — before any JSON parsing. Many frameworks parse the body automatically; configure them to pass raw bytes for your webhook route.

Receiving duplicate deliveries

VellumUp may retry failed deliveries. Make your handler idempotent by checking the article slug before inserting, or use a unique constraint on the slug column in your database.

Endpoint times out (delivery marked failed)

VellumUp waits up to 8 seconds for a response. Respond with HTTP 200 immediately, then process the payload asynchronously in a background job or queue.

Null fields in the payload

Fields like cover_image, meta_description, and focus_keyword can be null. Always handle null values explicitly in your code instead of assuming they are present.

Test delivery succeeds but real deliveries fail

Test events use a simplified payload. Ensure your handler handles all event types (article.published, article.updated, article.translated) and does not crash on unexpected fields.

Security Best Practices

Always verify the signature

Never process a payload without verifying the HMAC-SHA256 signature. This ensures the request came from VellumUp and was not tampered with.

Reject old timestamps

Discard any request where |now − timestamp| > 300 seconds. This prevents replay attacks where an attacker re-sends a previously captured valid request.

Use the raw request body

Sign the raw bytes as received — before any JSON parsing. Parsing and re-serialising can change whitespace and break the signature check.

Respond quickly, process async

Return HTTP 200 within a few seconds or VellumUp will mark the delivery as failed. Offload heavy processing (DB writes, rebuilds) to a background queue.

Integrations
VellumUp

AI-powered SEO articles that match your brand voice and rank.

Say Hello

support@vellumup.com

© 2026 VellumUp. All rights reserved.

Terms of Service·Privacy Policy·Accessibility