Receiving Blog Articles
Publish FoPost articles on your own site, whatever it is built with.
The Website target sends every published article to a URL you control, as one signed JSON request. Your site stores the article and renders it however it likes. Any framework works: all it needs is an HTTPS route that can read a raw request body.
Set it up
- Open Settings → Blog and choose Website as the target
- Enter the URL of your receiver, for example
https://yourbrand.com/api/fopost/articles. It has to behttps://and it has to answer directly; redirects are not followed - Copy the signing secret (
whsec_…) that appears. It is shown once. Keep it in your site's environment, next to your other secrets - Press Send test. FoPost sends a signed
article.testrequest, and the page tells you what your site answered
Lost the secret? Rotate it from the same page: the old one stops verifying at once and the new one is shown once.
What arrives
A POST with Content-Type: application/json and these headers:
| Header | Contents |
|---|---|
X-FoPost-Event | article.published, or article.test for the test |
X-FoPost-Delivery | An id for this delivery |
X-FoPost-Timestamp | Unix seconds, when the request was sent |
X-FoPost-Signature-256 | sha256= and the hex HMAC-SHA256 of <timestamp>.<rawBody>, keyed with your secret |
X-FoPost-Signature | sha256= and the hex HMAC-SHA256 of the raw body alone |
The body is the usual envelope, with the whole article in data:
{
"event": "article.published",
"data": {
"id": "b41e7c05-9d38-4a2f-8e60-1c7d5b3a9f24",
"title": "A Marathon Training Plan for First-Time Runners",
"slug": "marathon-training-plan-for-first-time-runners",
"excerpt": "Sixteen weeks, three runs a week, and one long run that grows slowly.",
"metaTitle": "Marathon Training Plan for Beginners (16 Weeks)",
"metaDescription": "A sixteen-week marathon plan for first-time runners.",
"bodyMarkdown": "## Why sixteen weeks\n\n...",
"bodyHtml": "<h2>Why sixteen weeks</h2><p>...</p>",
"featuredImage": { "mediaId": "6b0e9a4c-…", "url": "https://…", "alt": "A runner lacing shoes at dawn" },
"images": [{ "mediaId": "9c2f7b31-…", "url": "https://…", "alt": "A weekly mileage chart", "position": 2 }],
"tags": ["marathon", "training"],
"keyword": "marathon training plan for beginners",
"wordCount": 1540,
"publishedAt": "2026-10-14T07:00:00.000Z"
},
"timestamp": "2026-10-14T07:00:01.000Z"
}Use bodyHtml if your site renders HTML, bodyMarkdown if it has its own renderer. Both reference the images by the same URLs.
Image URLs stop working after 6 hours. Download every image (featuredImage and each entry in images) while handling the request and serve them from your own storage, then rewrite the URLs in the body you store.
Verify before you store
Anyone who finds the URL can send it JSON, so reject anything that does not verify:
- Read the body as raw bytes, before any JSON parsing. A re-serialised object is not byte-identical to what was sent
- Reject when
X-FoPost-Timestampis more than 5 minutes from now. This is what stops a captured request from being replayed later - Compute HMAC-SHA256 over
<timestamp>.<rawBody>with your secret and compare it toX-FoPost-Signature-256in constant time - Only then parse the JSON
import { createHmac, timingSafeEqual } from 'node:crypto';
const TOLERANCE_SECONDS = 300;
export function verifyArticleRequest(
rawBody: string,
headers: { timestamp: string | null; signature: string | null },
secret: string,
): boolean {
if (!headers.timestamp || !headers.signature) return false;
const sent = Number(headers.timestamp);
if (!Number.isFinite(sent)) return false;
if (Math.abs(Date.now() / 1000 - sent) > TOLERANCE_SECONDS) return false;
const expected =
'sha256=' + createHmac('sha256', secret).update(`${headers.timestamp}.${rawBody}`).digest('hex');
const a = Buffer.from(headers.signature.trim());
const b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
}A receiver on the web standard Request, which most frameworks expose or wrap:
export async function receiveArticle(request: Request): Promise<Response> {
const rawBody = await request.text();
const ok = verifyArticleRequest(
rawBody,
{
timestamp: request.headers.get('x-fopost-timestamp'),
signature: request.headers.get('x-fopost-signature-256'),
},
process.env.FOPOST_WEBSITE_SECRET!,
);
if (!ok) return new Response('invalid signature', { status: 401 });
const { event, data } = JSON.parse(rawBody);
if (event === 'article.test') {
return Response.json({ ok: true });
}
const url = await saveArticle(data); // store the article and its images, return the live page URL
return Response.json({ url });
}Header names are case-insensitive; read them however your framework hands them to you. The official SDKs ship a webhooks.verify helper that does the check above, including the timestamp rule, so you do not have to write it in every language.
Answer
A 2xx tells FoPost the article landed; anything else, or no answer within 15 seconds, marks the article failed with the status your site returned, and you can publish it again from the dashboard once the receiver is fixed.
Answering with JSON { "url": "https://yourbrand.com/blog/..." } is optional and worth doing: FoPost stores it as the article's publishedUrl, so the dashboard links straight to the live page and the article.published event your other webhooks receive carries it too. Only an https:// URL is kept.
Handle the test
article.test carries a sample article in the same shape, with tags: ["test"] and no images. Verify it exactly like a real one, then answer 2xx without storing anything. The settings page reports your response, so a 401 here means the secret or the raw-body handling is wrong, and a 404 means the route is not where you entered it.
Keep it idempotent
Store articles keyed by data.id, and overwrite on a repeat rather than inserting twice. A publish that is retried after a timeout on your side arrives again with the same id.
Next
Related documentation
- Guide
How to use FoPost as a creator, a team, or an agency.
- Getting Started
From signing up to your first published post.
- Dashboard Overview
A tour of the sidebar and what lives where.
- Connecting Accounts
Connect a network, keep it healthy, and reconnect when a token expires.
- Creating Posts
The composer, per-network overrides, and previews.
- Scheduling
Schedule a post, repeat it, and see what happened.
- The Calendar
Everything scheduled and published, in one view you can drag.
- Bulk Scheduling
Import a spreadsheet of posts, validate it, and roll it back if it is wrong.