Publishing webhook
How TangentFlow delivers a scheduled post to your site, and how to verify it came from us.
{"url": "https://…"} so we can record the live link. Three attempts, at least fifteen minutes apart, then the post is marked failed and you are emailed.
Events
| Event | When |
|---|---|
| test | You press "Send test event". Nothing is published. |
| post.publish | A scheduled post reaches its slot, or you press "Publish now". |
| post.update | An already published post is republished after an edit. |
| post.unpublish | You unpublish a post. |
Headers
content-type: application/json webhook-id: msg_5f1c0f9a1b7f4a9e8c2d3b40 webhook-timestamp: 1787200000 webhook-signature: v1,<base64 HMAC-SHA256> x-tangentflow-event: post.publish
The signed content is {id}.{timestamp}.{body}, keyed with your signing secret (shown once when you add the destination, rotatable any time). Reject requests whose timestamp is more than five minutes old, and compare signatures in constant time.
Payload
{
"event": "post.publish",
"sent_at": "2026-08-22T09:00:00.000Z",
"site": { "domain": "yoursite.com" },
"post": {
"id": 42,
"slug": "best-client-galleries-for-photographers-2026",
"title": "Best client galleries for photographers in 2026",
"excerpt": "Short plain-text summary.",
"markdown": "# Best client galleries…",
"html": "<h1>Best client galleries…</h1>",
"meta": {
"title": "Best client galleries for photographers in 2026",
"description": "≤155 character meta description",
"canonical": null,
"tags": ["guide"],
"jsonld": { "@context": "https://schema.org", "@type": "FAQPage" },
"last_updated": "2026-08-22"
},
"scheduled_for": "2026-08-22T09:00:00.000Z",
"version": 1,
"target_questions": ["auto-q3", "auto-q7"]
}
}
target_questions are the buyer questions this post was written to answer. We re-ask them on your weekly schedule and report whether mentions moved, so keep the ids if you want to correlate on your side.
Verifying in Node
import crypto from "node:crypto";
export function verify(headers, rawBody, secret) {
const id = headers["webhook-id"];
const ts = headers["webhook-timestamp"];
const sig = headers["webhook-signature"];
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = crypto.createHmac("sha256", key)
.update(id + "." + ts + "." + rawBody).digest("base64");
return sig.split(" ").some((part) =>
crypto.timingSafeEqual(Buffer.from(part.replace(/^v1,/, "")), Buffer.from(expected)));
}
WordPress instead
If you run WordPress, skip the webhook: add your site URL, a username and an application password (Users, then Profile, then Application Passwords) and we publish through the REST API. Test events are created as drafts.
Questions: bibin@tangentflow.com.