Docs

Publishing webhook

How TangentFlow delivers a scheduled post to your site, and how to verify it came from us.

We POST JSON to your endpoint when a post reaches its slot, signed with Standard Webhooks headers. Your endpoint returns any 2xx status, optionally with {"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

EventWhen
testYou press "Send test event". Nothing is published.
post.publishA scheduled post reaches its slot, or you press "Publish now".
post.updateAn already published post is republished after an edit.
post.unpublishYou 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.