Webhook payload
The exact JSON Findable POSTs to your webhook, the headers it carries, and how to verify and de-duplicate it.
Findable POSTs a signed JSON body to your configured webhook URL whenever an article finishes generating, and when you click "Send test event". See Publishing with a webhook for setup. This page is the reference for the request itself.
Event types
Every payload carries an event field, and the same value in the
X-Findable-Event header, so you can route without parsing the body.
event | When it fires |
|---|---|
article.published | An article finished generating. Sent once per article. |
article.updated | An article Findable already sent you was strengthened and re-sent. Same article_id and slug as the original. Only sent when you have turned on "My site can receive content updates" in Settings, Publishing. |
webhook.test | You clicked "Send test event" in Settings. |
Payload body
All fields are JSON. Empty fields are omitted from the body entirely, so a real request only contains the keys listed for its event below.
| Field | Type | Description |
|---|---|---|
event | string | Always present. article.published, article.updated, or webhook.test. |
article_id | string | Findable's stable ID for the article. Identical across retries of the same delivery; use it for de-duplication. |
title | string | The article title. |
slug | string | A URL-safe slug derived from the title (lowercase, alphanumeric and hyphens, capped at 80 chars). Use it as your post slug or compute your own from title. |
type | string | Article type: roundup, alternatives, comparison, howto, or explainer. If absent, treat it as explainer. |
markdown | string | The article body as Markdown. This is the canonical content article.published and article.updated ship today. |
excerpt | string | A short summary. Sent on test events. |
html | string | Reserved for rendered HTML. Not populated yet, so it is absent today. |
tags | array of strings | Reserved. Not populated yet, so it is absent today. |
generated_at | string | Always present. RFC 3339 timestamp in UTC, e.g. 2026-06-07T14:21:09Z. |
Example: article.published
{
"event": "article.published",
"article_id": "art_8fK2pQ7mZ1",
"title": "Best AI visibility tools for B2B SaaS",
"slug": "best-ai-visibility-tools-for-b2b-saas",
"type": "roundup",
"markdown": "# Best AI visibility tools for B2B SaaS\n\nWhen buyers ask an AI assistant...",
"generated_at": "2026-06-07T14:21:09Z"
}Example: article.updated
Identical in shape to article.published. The article_id and slug match the
delivery you already received, so your receiver updates the existing post rather
than creating a second one. generated_at is the time of the revision.
{
"event": "article.updated",
"article_id": "art_8fK2pQ7mZ1",
"title": "Best AI visibility tools for B2B SaaS",
"slug": "best-ai-visibility-tools-for-b2b-saas",
"type": "roundup",
"markdown": "# Best AI visibility tools for B2B SaaS\n\nWhen buyers ask an AI assistant...",
"generated_at": "2026-08-14T09:02:44Z"
}Content updates
Findable can strengthen an article it has already written for you and send you
the revised version, so one URL keeps improving instead of accumulating a second
article on the same topic. Revisions keep the same article_id, slug, and
title intent; the body is rewritten to answer its questions more directly and
with more specific detail.
This is off by default. Turn on My site can receive content updates in
Settings, Publishing to opt in. Before you do, make sure your receiver handles
article.updated by looking up the existing post for that article_id and
replacing its body. A receiver that treats every delivery as a new post will
publish a duplicate; a receiver that ignores unknown events simply drops the
revision, which is safe but wastes it.
An article is revised at most once every 30 days, and each revision counts against your monthly article allowance just like a new article does.
Example: webhook.test
{
"event": "webhook.test",
"title": "Findable webhook test",
"excerpt": "If you're seeing this, your webhook is wired up correctly.",
"generated_at": "2026-06-07T14:21:09Z"
}Headers
Every request carries:
Content-Type: application/json
User-Agent: Findable-Webhook/1.0
X-Findable-Event: article.published
X-Findable-Signature: sha256=<hmac>
X-Findable-Attempt: 1X-Findable-Event mirrors the body's event. X-Findable-Attempt is the
delivery attempt number, starting at 1 (see Delivery and retries below).
Verifying the signature
The signature is an HMAC-SHA256 of the raw request body, keyed with your
per-account signing secret (Settings, Publishing, "Signing secret"). To verify,
compute the same HMAC over the raw body and compare it, in constant time,
against the hex after sha256=.
import crypto from "node:crypto";
function verify(rawBody, header, secret) {
const expected =
"sha256=" +
crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
return crypto.timingSafeEqual(Buffer.from(header), Buffer.from(expected));
}Verify against the raw bytes, before any JSON parsing or re-serialization, or the hash will not match. Most receivers (such as a Zapier Catch Hook) skip verification and trust the URL. Verify only if you run a custom receiver and want defense against URL leakage. Rotating the secret (Settings, Publishing) invalidates the old one immediately, so update your receiver before the next delivery.
Delivery and retries
article.published is delivered asynchronously. Findable makes up to four
attempts: the first immediately, then retries after 1 minute, 5 minutes, and 30
minutes. A 2xx response stops the sequence. A 4xx (other than 429) is
treated as a configuration problem and is not retried; 429 and 5xx are
retried. Each attempt times out after 15 seconds.
article.updated uses the same delivery and retry schedule.
Because of retries, your receiver should be idempotent: the same article_id
can arrive more than once, for example if you return 2xx slowly and Findable
times out, then retries. De-duplicate on article_id within an event type. An
article.updated carrying an article_id you have already published is not a
duplicate: it is the revision, and it should overwrite the post.
Test events are different: "Send test event" delivers once, synchronously, with no retry, and shows the result (status code and a response excerpt) inline and in Recent deliveries.