Setup webhooks

Receive article events in your app and verify that they really came from ArticlesQ.

Add an endpoint

Open Settings → Integrations & API → Webhooks in your project, add the URL you want us to call, and pick the events you care about. We show you a signing secret starting with whsec_ — copy it into your app's environment as ARTICLESQ_WEBHOOK_SECRET.

Each endpoint gets its own secret, so staging and production never share one.

What we send

A POST with a JSON body and three headers:

HeaderValue
webhook-idUnique id for this delivery. Use it to ignore an event you already handled.
webhook-timestampSeconds since the epoch, part of the signature.
webhook-signaturev1,<base64 HMAC-SHA256> over <webhook-id>.<webhook-timestamp>.<body>.
Body
{
  "type": "article.published",
  "timestamp": "2026-06-01T09:00:00.000Z",
  "article": {
    "id": "clz9k2h1a0000abcd1234efgh",
    "slug": "best-crm-for-startups",
    "projectId": "proj_abc123"
  }
}

The event tells you what changed, not the content. Fetch GET /articles/{slug} for the article itself, so you always render the current version.

Verify the signature

We follow the Standard Webhooks specification, so most languages have an official library that verifies a delivery in one call.

Verification recomputes the HMAC from the id, the timestamp and the exact body you received, then compares it to the header in constant time. That is why you must pass the raw body: parsing and re-serialising it first changes the bytes, and the signature no longer matches.

app/webhooks/articlesq/route.ts
// npm install standardwebhooks
import { revalidatePath } from "next/cache";
import { Webhook, WebhookVerificationError } from "standardwebhooks";

export async function POST(request: Request) {
  const payload = await request.text();

  let event: { type: string; article: { slug: string } };
  try {
    event = new Webhook(process.env.ARTICLESQ_WEBHOOK_SECRET!).verify(payload, {
      "webhook-id": request.headers.get("webhook-id") ?? "",
      "webhook-timestamp": request.headers.get("webhook-timestamp") ?? "",
      "webhook-signature": request.headers.get("webhook-signature") ?? "",
    }) as { type: string; article: { slug: string } };
  } catch (error) {
    if (error instanceof WebhookVerificationError) {
      return Response.json({ error: "Invalid signature" }, { status: 401 });
    }
    return Response.json({ error: "Invalid payload" }, { status: 400 });
  }

  revalidatePath("/blog");
  revalidatePath(`/blog/${event.article.slug}`);

  return Response.json({ received: true });
}

In the Next.js example, revalidatePath marks those routes stale, so the next visitor gets a freshly rendered page carrying the new article instead of the copy built at deploy time. If your article data is fetched through a cache tag, call revalidateTag once instead of listing every path.

Answer first, work afterwards: we wait up to 5 seconds for a response, so a rebuild or a cache purge belongs after you reply (in Next.js, after() from next/server).

Testing your handler

Publish or edit an article in the dashboard, then check your logs for the delivery. A 401 means the secret in your app does not match the endpoint's, so rotate it in settings and copy the new value across.

On this page