Webhooks
Receive each post as signed JSON at your own endpoint: events, the payload, signatures and retries.
Each post is sent as signed JSON to your own endpoint, for any custom site or backend. For Zapier or Make, connect those instead (Zapier, Make): they send the same requests, with the address checked and a sample post when you connect.
Events
Every request is a POST with Content-Type: application/json and these headers:
X-WritingStudio-Event:ping,post.publishedorpost.updatedX-WritingStudio-Signature: see belowUser-Agent: WritingStudio/1.0
ping is sent when you connect. A new post:
{ "event": "post.published", "id": null, "sentAt": "2026-10-28T09:00:00.000Z",
"post": { "title": "…", "slug": "…", "html": "…", "excerpt": "…", "metaTitle": "…", "metaDescription": "…",
"image": { "url": "…", "alt": "…" }, "author": "…", "publishedAt": "…" } }
An update (a refresh, or an edit published again) is post.updated with the id your endpoint gave the post.
Your answer
Answer 2xx to accept. You can answer { "id": "…", "url": "…" } to name the post on your side; updates then carry that id, and the article links to url. Without an id, the slug is used.
Signatures
X-WritingStudio-Signature: t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of <t>.<raw body> with your signing secret. Check it before saving anything, and refuse requests older than five minutes:
import crypto from 'node:crypto';
const [, t] = /t=(\d+)/.exec(sig) ?? [], [, v1] = /v1=([a-f0-9]{64})/.exec(sig) ?? [];
const want = crypto.createHmac('sha256', process.env.WRITING_STUDIO_SECRET).update(`${t}.${rawBody}`).digest('hex');
const ok = v1 && Math.abs(Date.now() / 1000 - Number(t)) < 300 && crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(want));
Retries
A 5xx, a 429 or no answer is tried again twice within seconds, then after 5 minutes, 30 minutes and 2 hours. A 401 or 403 stops at once (check the secret). If a post still can't be delivered, we email you.
Can’t find what you need? Email us.