Skip to main content
Webhooks provide a powerful way to keep your site’s content fresh and automate workflows. When an event occurs in your Marble workspace, such as publishing a post, Marble sends an HTTP POST payload to a URL you configure. Webhooks are processed asynchronously by Marble’s background jobs worker. Marble records events, retries failed deliveries, and stores delivery attempts so webhook activity can be inspected later.

Content revalidation

For statically generated sites, content is fetched at build time. If you update a post in Marble, the change won’t be live until you trigger a new deployment. Frameworks like Next.js provide built-in solutions for this, such as Incremental Static Regeneration (ISR), which can be triggered on-demand by a webhook for instant updates.

Automating workflows

Create a serverless function that listens for a post.published event from a Marble webhook. When triggered, your function can:
  • Send a newsletter to your subscribers with the new post
  • Share the post on social media
  • Sync the content to another service or backup location

Setting up webhooks in Marble

1

Navigate to Webhooks

In the Marble dashboard, open your workspace, go to Settings, then choose Webhooks under the Developers section.
2

Create a New Webhook

Click Create Webhook and fill in the required details:
  • Name: A descriptive name for your webhook.
  • URL: The endpoint where the webhook payload will be sent.
  • Format: Choose JSON, Discord, or Slack.
  • Events: Select the events you want to listen for, such as post.published or post.updated.
3

Save and Copy Secret

Save your webhook, then open it from the list by clicking the 3 dots and selecting Details. On the webhook details page, click Copy secret next to the masked secret value. You’ll need this to verify the authenticity of incoming requests.

Inspecting webhook deliveries

Every time an event matches a webhook’s subscription, Marble queues a delivery and stores the result. You can inspect this history from the webhook details page in the dashboard. To open it, go to Settings → Webhooks, click the 3 dots on the webhook’s row, and choose Details. The details page is split into two parts:
  • Webhook settings (side panel): edit the webhook’s name, URL, payload format, enabled state, and subscribed events. Changes apply to future deliveries only.
  • Deliveries: a paginated table of every delivery attempted for this webhook, with filters and expandable rows.

Filtering deliveries

Above the deliveries table you can filter by:
  • Status: pending, sending, success, retrying, or failed.
  • HTTP response: 2xx, 3xx, 4xx, 5xx, or No response (the endpoint never responded, for example on a network error or timeout).
  • Event type: any of the events the webhook is subscribed to, such as post.published or tag.created.
Filters can be combined and the results are paginated.

Reading a delivery

Click a row to expand it and see the full delivery record:
  • Event ID and event timestamp: identify the workspace event that triggered the delivery.
  • Delivery ID: matches the x-marble-delivery-id header your endpoint received.
  • Sent at: when the most recent attempt was sent.
  • HTTP response: the status code returned by your endpoint, or No response if the request never completed.
  • Duration: how long the latest attempt took, in milliseconds.
  • Error message: shown when the latest attempt failed, for example with a connection error or non-2xx response body.
  • Payload: the exact JSON body that was sent. Use Copy payload to copy it for replay or debugging.
Use this view to confirm whether your endpoint received an event, what it returned, and what payload it was given.

Sending a test webhook

To verify that your endpoint is reachable and that your signature verification works without waiting for a real content event, click Send test webhook on the details page. Marble queues a test delivery against the webhook’s configured URL, and a new row appears in the deliveries table once it is sent. This is useful when:
  • You’ve just created a webhook and want to confirm the URL and secret are wired up correctly.
  • You’ve changed your endpoint’s deployment or signature verification code.
  • A previous delivery failed and you want to retry against the current endpoint without editing real content.

Verifying webhook requests

To ensure that incoming webhook requests are from Marble and haven’t been tampered with, you can verify the request using the webhook secret. Here’s an example of how to verify a webhook request in Next.js using the crypto module:
// lib/marble/webhook.ts
import { createHmac, timingSafeEqual } from "node:crypto";
import { revalidatePath, revalidateTag } from "next/cache";
import type { PostEventData } from "@/types/blog";

export async function handleWebhookEvent(payload: PostEventData) {
  const event = payload.type;
  const data = payload.data;

  // Handle any post.* events (published, updated, deleted, etc.)
  if (event.startsWith("post")) {
    // Revalidate the blog index and the single post page
    revalidatePath("/blog");
    revalidatePath(`/blog/${data.slug}`);

    // If your data fetches use tags, revalidate that tag as well:
    // e.g. fetch(..., { next: { tags: ["posts"] } })
    revalidateTag("posts");

    return {
      revalidated: true,
      now: Date.now(),
      message: "Post event handled",
    };
  }

  return {
    revalidated: false,
    now: Date.now(),
    message: "Event ignored",
  };
}

export function verifySignature(
  secret: string,
  signatureHeader: string,
  bodyText: string
) {
  // Strip possible "sha256=" prefix
  const expectedHex = signatureHeader.replace(/^sha256=/, "");

  const computedHex = createHmac("sha256", secret)
    .update(bodyText)
    .digest("hex");

  // Convert to buffers for constant-time compare
  const expected = Buffer.from(expectedHex, "hex");
  const computed = Buffer.from(computedHex, "hex");

  // lengths must match for timingSafeEqual
  if (expected.length !== computed.length) return false;

  return timingSafeEqual(expected, computed);
}
// app/api/revalidate/route.ts
import { NextResponse } from "next/server";
import type { PostEventData } from "@/types/blog";
import { verifySignature, handleWebhookEvent } from "@/lib/marble/webhook";

export async function POST(request: Request) {
  const signature = request.headers.get("x-marble-signature");
  const secret = process.env.MARBLE_WEBHOOK_SECRET;

  if (!secret || !signature) {
    return NextResponse.json(
      { error: "Secret or signature missing" },
      { status: 400 }
    );
  }

  const bodyText = await request.text();

  if (!verifySignature(secret, signature, bodyText)) {
    return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
  }

  let payload: PostEventData;

  try {
    payload = JSON.parse(bodyText) as PostEventData;
  } catch {
    return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
  }

  if (!payload.type || !payload.data) {
    return NextResponse.json(
      { error: "Invalid payload structure" },
      { status: 400 }
    );
  }

  try {
    const result = await handleWebhookEvent(payload);
    return NextResponse.json(result);
  } catch (err) {
    return NextResponse.json(
      { error: "Failed to process webhook" },
      { status: 500 }
    );
  }
}
If you want a better guide on how to invalidate cache in your framework of choice, you can check out our blog post on Using Marble’s Webhooks with the Next.js App Router.
Marble will send the signature in the x-marble-signature header of the request. You can use this signature to verify the authenticity of the request. Make sure to add the process.env.MARBLE_WEBHOOK_SECRET environment variable.

Request Payload

When a webhook is triggered, Marble sends a POST request to the specified URL with a JSON payload. JSON webhooks receive a stable envelope that includes the event metadata, the affected resource, the actor when available, and event-specific data. Webhook requests include these headers:
  • x-marble-event: The event type, such as post.updated.
  • x-marble-event-id: The ID of the workspace event.
  • x-marble-delivery-id: The ID of the delivery attempt.
  • x-marble-timestamp: The Unix timestamp for the request.
  • x-marble-signature: The HMAC SHA-256 signature for the request body.
Here are a few post event examples:
{
  "id": "evt_cmf3d1gsv11469tlkp53bcutv",
  "type": "post.published",
  "createdAt": "2026-05-22T14:42:31.120Z",
  "workspaceId": "org_cms96emp70001l60415sft0i5",
  "resource": {
    "type": "post",
    "id": "post_cmf3d1gsv11469tlkp53bcutv"
  },
  "actor": {
    "type": "api_key",
    "id": "key_cmf3cp0nz0001l604a8c7h2sc"
  },
  "data": {
    "id": "post_cmf3d1gsv11469tlkp53bcutv",
    "title": "Getting Started with Marble CMS",
    "slug": "getting-started-with-marble",
    "description": "Learn how to publish your first post with Marble.",
    "coverImage": "https://cdn.marblecms.com/images/getting-started.png",
    "status": "published",
    "featured": false,
    "categoryId": "cat_cmf3cujdp0003l604w7h3whg9",
    "primaryAuthorId": "author_cmf3cvj1e0004l604x7m6g6fa",
    "publishedAt": "2026-05-22T14:00:00.000Z",
    "createdAt": "2026-05-21T18:12:09.431Z",
    "updatedAt": "2026-05-22T14:42:30.951Z"
  }
}
The top-level envelope is the same for post.published, post.updated, post.deleted, tag.created, category.updated, author.deleted, and other JSON events. The type, resource, and data fields change to match the event. Post payloads include id, title, slug, description, coverImage, status, featured, categoryId, primaryAuthorId, publishedAt, createdAt, and updatedAt. Update events also include changes, an array of field names that changed. Category and tag payloads include id, name, slug, description, createdAt, and updatedAt. Author payloads include profile fields such as name, slug, bio, role, image, email, and socials. Media payloads use name and media metadata such as url, alt, type, size, mimeType, dimensions, duration, and blurHash.

Event Types

Marble supports a variety of event types that you can listen for with webhooks. Here are the currently available events:
  • post.published: Triggered when a post is published.
  • post.unpublished: Triggered when a post is unpublished.
  • post.updated: Triggered when a post is updated.
  • post.deleted: Triggered when a post is deleted.
  • tag.created: Triggered when a new tag is created.
  • tag.updated: Triggered when a tag is updated.
  • tag.deleted: Triggered when a tag is deleted.
  • category.created: Triggered when a new category is created.
  • category.updated: Triggered when a category is updated.
  • category.deleted: Triggered when a category is deleted.
  • media.uploaded: Triggered when a media file is uploaded.
  • media.updated: Triggered when a media file is updated.
  • media.deleted: Triggered when a media file is deleted.
  • author.created: Triggered when a new author is created.
  • author.updated: Triggered when an author is updated.
  • author.deleted: Triggered when an author is deleted.
With more events planned for the future, you can stay tuned for updates in the Marble documentation.