Quick start
- Create an HTTPS endpoint that accepts
POSTrequests with a JSON body. - In Postbing, open Integrations, choose Webhook and paste the endpoint address. Postbing sends a signed
pingto check it answers. - Copy the signing secret shown once after connecting. Store it on your server as
POSTBING_WEBHOOK_SECRET. - Verify every request, then save the article. Answer with any
2xxstatus within 30 seconds.
Endpoints must be public http(s) addresses. Redirects are not followed, so give the final address. Private and internal network addresses are refused.
Events
| Event | When |
|---|---|
article.publish | A new article goes live. |
article.update | An article that is already live was edited and pushed again. article.id is the id you returned the first time. |
page.publish, page.update | The same for standalone pages (local landing pages), which you may want to route differently. |
article.unpublish | The article was taken down in Postbing. Remove or hide it. |
ping | Connection checks and the Test connection button. Answer 2xx; test: true marks a manual test. |
Headers
| Header | Value |
|---|---|
X-Postbing-Event | The event name, as above. |
X-Postbing-Timestamp | Unix seconds when the request was signed. |
X-Postbing-Signature | v1=<hex>: HMAC-SHA256 of <timestamp>.<raw body> with your secret. Right after you make a new secret it carries two values, comma separated, for 24 hours; accept the request if either matches. |
Idempotency-Key | The same on every retry of the same delivery. Use it to ignore repeats. |
X-Postbing-Delivery | A unique id for this attempt (useful in your logs). |
X-Postbing-Article-Id | The Postbing article id (article events). |
Until February 11, 2027 we also send the two legacy event and signature headers from before our rename (sha256= HMAC of the body only) so existing receivers keep working. Move to X-Postbing-Signature before then: it includes the timestamp, which stops replayed requests.
Payload
Version 2. Fields may be added over time; ignore the ones you don't use.
{
"event": "article.publish",
"version": 2,
"id": "4b0c2f0e-6c1d-4a59-9f0b-1f8b6a2b9d11",
"created_at": "2026-11-13T09:00:00Z",
"idempotency_key": "pb-1842-publish-9c1f2a7d5e3b4c60",
"mode": "publish",
"article": {
"id": null,
"postbing_id": 1842,
"type": "article",
"title": "How to choose a coffee grinder",
"slug": "how-to-choose-a-coffee-grinder",
"html": "<p>...</p><script type=\"application/ld+json\">...</script>",
"body_html": "<p>...</p>",
"markdown": "...",
"excerpt": "Burr or blade, flat or conical ...",
"meta_title": "How to choose a coffee grinder (2026 guide)",
"meta_description": "Burr or blade, flat or conical ...",
"faqs": [{"question": "...", "answer": "..."}],
"schema_jsonld": {"@context": "https://schema.org", "@graph": []},
"tags": ["grinders"],
"categories": ["Brewing guides"],
"language": null,
"scheduled_date": "2026-11-13",
"featured_image": {"url": "https://...", "alt": "..."},
"images": [{"url": "https://...", "alt": "...", "kind": "featured"}]
}
}htmlincludes the JSON-LD script;body_htmlis the article alone, for templates that print schema themselves.- Save by
postbing_id(upsert). That makes repeated deliveries harmless. - To link the post back, answer with JSON such as
{"id": "your-post-id", "url": "https://yoursite.com/blog/how-to-choose"}. We store both: the id comes back asarticle.idon updates and the URL shows in Postbing as the live link.
Verify the signature
Compute the HMAC over the raw request body, before any JSON parsing, and reject requests whose timestamp is more than 5 minutes from your clock.
const crypto = require("crypto");
// Express: use the raw body, e.g.
// app.post("/postbing-webhook", express.raw({ type: "*/*" }), handler)
function verifyPostbing(req, secret = process.env.POSTBING_WEBHOOK_SECRET) {
const ts = req.get("X-Postbing-Timestamp") || "";
if (!/^\d+$/.test(ts)) return false;
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // replay window
const expected = crypto.createHmac("sha256", secret)
.update(Buffer.concat([Buffer.from(ts + "."), req.body]))
.digest("hex");
return (req.get("X-Postbing-Signature") || "").split(",").some((part) => {
const sig = part.trim().replace(/^v1=/, "");
return sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
});
}import hashlib, hmac, os, time
from flask import request
def verify_postbing(secret=os.environ["POSTBING_WEBHOOK_SECRET"]):
ts = request.headers.get("X-Postbing-Timestamp", "")
if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
return False
body = request.get_data() # the raw bytes, before any JSON parsing
expected = hmac.new(secret.encode(), ts.encode() + b"." + body, hashlib.sha256).hexdigest()
sigs = request.headers.get("X-Postbing-Signature", "").split(",")
return any(hmac.compare_digest(s.strip().removeprefix("v1="), expected) for s in sigs)<?php
function postbing_verify(string $secret): bool {
$ts = $_SERVER['HTTP_X_POSTBING_TIMESTAMP'] ?? '';
if (!ctype_digit($ts) || abs(time() - (int) $ts) > 300) return false;
$body = file_get_contents('php://input');
$expected = hash_hmac('sha256', $ts . '.' . $body, $secret);
foreach (explode(',', $_SERVER['HTTP_X_POSTBING_SIGNATURE'] ?? '') as $part) {
if (hash_equals($expected, preg_replace('/^v1=/', '', trim($part)))) return true;
}
return false;
}
// postbing_verify(getenv('POSTBING_WEBHOOK_SECRET')) or http_response_code(401);Retries and duplicates
- Any non-
2xxanswer, a timeout (30 seconds) or a network error is retried after 5 minutes, 30 minutes and 2 hours. After that the article is marked as failed in Postbing with the reason, and the site owner gets an email. 408,409,425,429and5xxare retried. Other4xxanswers stop at once, so answer400for a payload you will never accept.- A retry can arrive even when your first answer was a success that got lost on the way. The same
Idempotency-Keyandpostbing_idmake it safe to ignore or apply again. - Every delivery, with its status code and timing, is listed in Postbing under Integrations, the site's menu, Delivery log.
Recipes
These platforms have no native Postbing app yet. Each recipe needs a developer for about an hour.
Wix Wix Blog API
- Create a site API key in your Wix account (API Keys) with the Wix Blog permissions, and note your site id.
- Deploy a small endpoint (a Wix HTTP function, a Cloudflare Worker or any server) that verifies the signature as above.
- Convert
article.body_htmlto Wix rich content with Wix's Ricos conversion endpoint, then create a draft post with the title, slug and a site member as the author, and publish it. - Answer with
{"id": "<wix post id>", "url": "<post url>"}; use the id to update the same post onarticle.update.
Framer CMS collection
- In Framer, create a CMS collection with Title, Slug, Content (formatted text), Excerpt and Image fields, and create a Server API key for the project (Site settings, General).
- Run a small Node service with Framer's
framer-apipackage that verifies the signature, upserts the item bypostbing_id(store it in a plain text field) and publishes the site. - Framer's Server API is in open beta; check Framer's changelog before relying on it in production.
Next.js and custom sites Any stack
- Add a route handler, for example
app/api/postbing/route.ts. Read the body withawait req.text()so you can verify the raw bytes, thenJSON.parseit. - Upsert into your database or markdown folder by
postbing_id; useslugfor the URL andmeta_title/meta_descriptionfor your page metadata. - Revalidate the page (
revalidatePath) so the article is live at once, and answer with its URL.
Zapier or Make No code
- Create a Zap with Webhooks by Zapier, Catch Raw Hook (or a Make custom webhook) and paste its address into Postbing.
- Check the signature in a Code step (the Python version above runs in Zapier's Code by Zapier), then map
article.title,article.body_htmlandarticle.sluginto the app you publish with. - Catch hooks can't answer with your post id, so updates arrive as
article.updatewithpostbing_id: look the post up by that id in your Zap.
Testing
Use Test connection on the site in Integrations to send a signed ping with "test": true and see your endpoint's answer. Lost the secret? Make a new one from the site's menu; the old one keeps working for 24 hours.