Webhook Events

When you publish a blog, update it, delete it, or complete a product optimization, WUDO sends a signed HTTP POST to your webhook endpoint. This page documents every event type and its payload.

New to webhooks? Start with the Auto-Publish Webhook Setup guide to create your endpoint and configure signature verification.

How Delivery Works

Every webhook request includes three headers:

X-Wudo-Signature: sha256={hex_digest}
X-Wudo-Event: blog.published
User-Agent: Wudo-Webhook/1.0
  • Signature — HMAC-SHA256 of the raw request body using your webhook secret
  • Content-Type — always application/json
  • Field naming — all payload fields use snake_case, null values are omitted
  • Timeout — WUDO waits up to 120 seconds for your endpoint to respond (to allow image downloads)

Events

POSTblog.published

Sent when you click "Publish" in the dashboard. Contains the full blog content, images, CSS, JavaScript, and all SEO metadata needed to render the post on your website.

{
  "event": "blog.published",
  "timestamp": "2026-02-15T14:30:00Z",
  "data": {
    "title": "10 Web Design Trends for 2026",
    "slug": "10-web-design-trends-2026",
    "html_content": "<article>Full HTML content...</article>",
    "css_content": "body { ... }",
    "js_content": "...",
    "excerpt": "Discover the latest web design trends...",
    "meta_title": "10 Web Design Trends for 2026 | YourSite",
    "meta_description": "Discover the latest web design trends...",
    "author": "Wudo AI",
    "tags": ["web design", "trends"],
    "keywords": ["web design", "2026", "trends"],
    "category": "Web Design",
    "featured_image_url": "https://wudoseo.com/api/v1/blogs/images/{id}/banner.webp",
    "og_image_url": "https://wudoseo.com/api/v1/blogs/images/{id}/og.webp",
    "pdf_url": "https://.../{id}/takeaway.pdf",
    "schema_org_json": "{...}",
    "reading_time_minutes": 8,
    "word_count": 3500,
    "publish_immediately": true,
    "images": [
      {
        "url": "https://wudoseo.com/api/v1/blogs/images/{id}/section-1.webp",
        "alt_text": "Modern dashboard layout example",
        "type": "section"
      }
    ]
  }
}
POSTblog.updated

Sent when you update a published blog. Contains only the changed fields (typically slug, html_content, css_content, js_content). Merge with your existing blog data.

{
  "event": "blog.updated",
  "timestamp": "2026-02-15T15:00:00Z",
  "data": {
    "slug": "10-web-design-trends-2026",
    "html_content": "<article>Updated HTML...</article>",
    "css_content": "body { ... }",
    "js_content": "..."
  }
}
POSTblog.deleted

Sent when a blog is deleted from the dashboard. Only contains the slug so you can remove it from your website.

{
  "event": "blog.deleted",
  "timestamp": "2026-02-15T16:00:00Z",
  "data": {
    "slug": "10-web-design-trends-2026"
  }
}
POSTproducts.optimized

Sent when a product optimization batch completes. Contains the optimized SEO data for each product in the batch.

{
  "event": "products.optimized",
  "timestamp": "2026-02-15T14:30:00Z",
  "data": {
    "products": [
      {
        "external_id": "prod-123",
        "meta_title": "Premium Blue Cotton T-Shirt | Soft & Breathable",
        "meta_description": "Experience ultimate comfort...",
        "page_title": "Premium Blue Cotton T-Shirt",
        "page_description": "<p>Rich HTML content...</p>",
        "image_url": "https://...",
        "handle": "blue-cotton-t-shirt"
      }
    ],
    "total_count": 1,
    "exported_count": 1,
    "failed_count": 0
  }
}
POSTtest

Sent when you click "Test Connection" in Settings → Integrations. Use this to verify your endpoint is reachable and signature verification works.

{
  "event": "test",
  "timestamp": "2026-02-15T14:30:00Z",
  "data": {
    "message": "Webhook receiver is working"
  }
}

Blog Payload Fields

FieldTypeDescription
titlestringBlog title
slugstringURL-safe slug (use as your blog URL path)
html_contentstringFull HTML (may be a complete document with <html> — extract <body> content)
css_contentstring?CSS styles (scope to avoid leaking into your page layout)
js_contentstring?JavaScript for interactive elements
excerptstring?Short excerpt for blog listing cards
meta_titlestring?SEO title for <title> tag
meta_descriptionstring?SEO description for meta tag
authorstring?Author name (configurable on Samurai+ plans)
tagsstring[]?Blog tags
keywordsstring[]?SEO keywords
categorystring?Blog category
featured_image_urlstring?Full URL to the featured/banner image
og_image_urlstring?Full URL to 1200x630 social sharing image
pdf_urlstring?Full URL to downloadable PDF takeaway
schema_org_jsonstring?JSON-LD structured data for SEO
reading_time_minutesint?Estimated reading time
word_countint?Total word count
publish_immediatelyboolWhether the blog should go live immediately
imagesarray?All images with url, alt_text, and type

Integration Tips

Extract body from HTML

The html_content field may contain a full HTML document (starting with <!DOCTYPE html>). Extract only the <body> content to embed it in your page layout. Also check for <style> tags in the <head> and merge them with css_content.

Scope the CSS

The CSS may contain global selectors like body, :root, or *. Wrap your blog in a container (e.g. .blog-content) and rewrite these selectors to target that container instead, so the blog styles don't affect your main site layout.

Download images locally

Image URLs in the payload point to wudoseo.com. For best performance, download them to your own storage and rewrite the URLs in the HTML content. This also ensures your blog works if the WUDO API is temporarily unavailable.

Handle partial updates

The blog.updated event only includes changed fields. Merge the update with your existing stored blog data rather than replacing the entire blog.

Signature Verification

Node.js / Next.js
import crypto from 'crypto';

function verifySignature(rawBody: string, signature: string, secret: string): boolean {
  if (!signature?.startsWith('sha256=')) return false;

  const receivedHex = signature.slice('sha256='.length);
  const computedHex = crypto
    .createHmac('sha256', secret)
    .update(rawBody, 'utf8')
    .digest('hex');

  try {
    return crypto.timingSafeEqual(
      Buffer.from(receivedHex, 'hex'),
      Buffer.from(computedHex, 'hex')
    );
  } catch {
    return false;
  }
}

// Usage:
const body = await request.text();
const signature = request.headers.get('X-Wudo-Signature') ?? '';
if (!verifySignature(body, signature, process.env.WUDO_WEBHOOK_SECRET!)) {
  return new Response('Invalid signature', { status: 401 });
}
const payload = JSON.parse(body);

Need help?

  • See the Webhook Setup guide for step-by-step configuration
  • See Code Examples for complete implementations in multiple languages
  • Contact us at contact@wudoseo.com