Webhook quickstart
Connect GrowWriter to your own website and get your first signed test event delivered, on one page, with one file of code.
Last updated on
This page gets you from nothing to Test event delivered. One stack, one file, no theory. When the test passes, the guide shows you how to save the articles that follow, and the payload reference lists everything they contain.
This page is for the person who can add a file to your website and deploy it. If that is not you, send them this link; it takes them one file. The code is for Next.js, the setup behind most Vercel and Netlify sites. Other stacks are in the guide; the steps on this page stay the same.
Connect the webhook in GrowWriter
Open the Publish page, find the Webhook card under Automation, and click Connect.
Enter the address the code will answer on. For the file below that is your
site's domain plus /webhooks/growwriter, for example
https://www.example.com/webhooks/growwriter. Use the exact address your
site serves: if it redirects example.com to www.example.com, enter the
www one. Click Create Connection.
The dialog now shows your Signing Secret, a string starting with
whsec_. Click Copy secret. It is shown only once, so keep the dialog
open or paste it somewhere safe.
Add the receiver file
Create this file in your project. It reads each request exactly as it arrived, checks that GrowWriter signed it, and answers the test event.
import { createHmac, timingSafeEqual } from 'node:crypto';
// A signed request older than this is rejected, so a captured request
// cannot be replayed later.
const MAX_AGE_SECONDS = 5 * 60;
function isSignedByGrowWriter(rawBody: string, header: string, secret: string) {
// The header looks like: t=1756718400,v1=5257a869e7...
const parts = Object.fromEntries(
header.split(',').map((part) => part.split('='))
);
const age = Math.abs(Date.now() / 1000 - Number(parts.t));
if (!(age < MAX_AGE_SECONDS)) {
return false;
}
// Recompute the fingerprint over "timestamp.body" with your WHOLE
// secret (the whsec_ prefix included) and compare in constant time.
const expected = createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`)
.digest('hex');
const provided = Buffer.from(parts.v1 ?? '', 'hex');
const wanted = Buffer.from(expected, 'hex');
return provided.length === wanted.length && timingSafeEqual(provided, wanted);
}
export async function POST(request: Request) {
// Read the body as text first. Parsing it before checking the signature
// changes the bytes and the check fails.
const rawBody = await request.text();
const signature = request.headers.get('x-growwriter-signature') ?? '';
const secret = process.env.GROWWRITER_WEBHOOK_SECRET ?? '';
if (!isSignedByGrowWriter(rawBody, signature, secret)) {
return new Response('Invalid signature', { status: 401 });
}
const payload = JSON.parse(rawBody);
// "Send test event" sends a ping with no article in it. Answering 200
// is what makes the test succeed.
if (payload.event === 'ping') {
return new Response('pong');
}
// A real article (or its removal: article.unpublished carries only the
// id). For now, just prove it arrived; the guide shows how to save it.
// Once you do save it, Response.json({ url }) with the post's public
// URL gives the publish dialog a "View post" link.
console.log(`${payload.event}: ${payload.article.id}`);
return new Response('OK');
}Give your site the secret and deploy
Your code reads the secret from an environment variable: a setting your hosting provider keeps outside your code. In your host's dashboard (Vercel: Settings, then Environment Variables) add:
GROWWRITER_WEBHOOK_SECRET=whsec_paste_your_secret_herePaste the whole value, whsec_ included. Then deploy your site so the new
file and the new variable are live.
Send the test event
Back in the GrowWriter dialog, click Send test event. GrowWriter signs a
small ping and posts it to your address. When your file answers, the dialog
shows Test event delivered. Click Done.
If it fails, the dialog names the reason. The two you are most likely to see:
- Your endpoint URL redirects. Enter the address it redirects to and try
again: your site answers on a different address (usually
wwwversus nowww), and the message shows which one to use. Click Edit URL on the webhook row to change it. - Your endpoint rejected the delivery: with a
401, the secret on your host does not match; with a404, the file is not at the address you entered.
Everything else is on the troubleshooting page.
Prove the check works
Optional, but worth a minute. This script signs a ping exactly the way GrowWriter does and sends it to your site:
SECRET="whsec_paste_your_secret_here"
URL="https://www.example.com/webhooks/growwriter"
BODY='{"version":"2026-09-06","event":"ping","deliveryId":"9d2f7c1e-6a3b-4c8d-b5e0-1f2a3b4c5d6e","sentAt":"2026-09-01T09:30:00.000Z"}'
TIMESTAMP=$(date +%s)
SIGNATURE=$(printf '%s.%s' "$TIMESTAMP" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')
curl -X POST "$URL" \
-H "Content-Type: application/json" \
-H "X-GrowWriter-Event: ping" \
-H "X-GrowWriter-Delivery: 9d2f7c1e-6a3b-4c8d-b5e0-1f2a3b4c5d6e" \
-H "X-GrowWriter-Signature: t=$TIMESTAMP,v1=$SIGNATURE" \
--data "$BODY"You should get pong. Now change one character inside BODY and run it
again: you should get 401. That is your signature check refusing a
request that was tampered with, which is the whole point.
What you have now
A public endpoint that only accepts requests signed by GrowWriter, and a
connection that shows Connected on the Channels page. Publishing an
article will deliver it to this file as an article.published event, and
the console.log line will show its id in your host's logs.
Next: save the article
The file above only logs the article. To store it, read Save the article into your CMS in the guide: six short rules about ids, dates, published state, keywords, images and escaping, each learned from a real receiver that got them wrong. The full receiver examples there apply all six.
Curious why the signature and the secret exist at all? The guide explains it in four plain steps.
Publish to BaseHub
Connect your BaseHub repository so every article becomes a row in a collection your site already reads from. No code needed.
Publish with webhooks
Send every article you publish in GrowWriter straight to your own website. Set up the webhook integration step by step, with ready-to-use code.