Custom webhook: publish Bangers articles to your own site

Connect any HTTPS endpoint as a Bangers publishing channel. Signed payloads, a ping on connect, and copy-paste verification for Node.js and Python.

Bangers publishes to a long list of networks, but not to the one destination every business already has: its own site. The Custom webhook channel closes that gap. Connect an HTTPS endpoint, and Bangers will POST every approved article to it. No platform-specific integration is required on your side.

What it is

Custom webhook is a native Bangers publishing channel, alongside Telegram, Bluesky, Mastodon and the rest. Where those adapters speak a platform's API, this one speaks HTTP to a URL you control. It works with any CMS that accepts an HTTP callback, a static-site rebuild hook, or a catch-hook node in n8n, Zapier or Make. If your site is built on Astro, Next, Hugo, Webflow, Notion, or something custom, this is the way Bangers reaches it.

Once connected, an article approved in Bangers is sent to your endpoint the same way a post is sent to any other channel: drafted, reviewed, approved, delivered. The channel accepts long-form content: a title and a body, where the body may contain Markdown.

Connecting it in the app

In Bangers, go to Channels → Custom webhook and provide two things:

  • Endpoint URL: must be HTTPS, and must resolve to a public host. Local addresses and private IP ranges are rejected.
  • Signing secret: at least 16 characters. There's a Generate button that fills in 32 random bytes as hex and shows it in plain text once, so you can copy it before you save the connection.

When you connect, Bangers immediately signs and sends a ping event to your endpoint. Your server needs to respond with any 2xx status for the connection to complete. If your endpoint isn't live yet, or it rejects the ping, the connection fails and you can fix it and retry. Nothing publishes until the ping succeeds.

Headers

Every request Bangers sends, ping or post.publish, carries the same headers:

Header Description
Content-Type Always application/json
User-Agent Bangers-Webhook/1
X-Bangers-Event post.publish or ping
X-Bangers-Delivery Opaque delivery id, ≤64 characters
X-Bangers-Timestamp Unix timestamp (seconds) the request was signed at
X-Bangers-Signature v1=<hex HMAC-SHA256 of the request>

Payloads

ping: sent on connect, and any time you want to test the endpoint manually:

{
  "event": "ping",
  "version": 1,
  "deliveryId": "…",
  "sentAt": "2026-09-06T12:00:00.000Z",
  "channel": { "name": "blog.example.com", "platform": "webhook" }
}

post.publish: sent for each approved article:

{
  "event": "post.publish",
  "version": 1,
  "deliveryId": "…",
  "sentAt": "2026-09-06T12:00:00.000Z",
  "channel": { "name": "blog.example.com", "platform": "webhook" },
  "post": {
    "format": "article",
    "title": "…",
    "body": "… markdown …",
    "text": null,
    "parts": null,
    "media": [{ "url": "https://…", "type": "image" }]
  }
}

post.format is article (uses title and body), post (uses text), or thread (uses parts). The fields not used by a given format are null, so check format before reading. media lists only publicly reachable URLs; anything held only inside Telegram can't be exported and is omitted from the array.

Reply contract

Your endpoint can optionally reply with JSON:

{ "url": "https://yoursite.com/blog/your-post-slug", "id": "internal-id" }

If you return a url, Bangers uses it as the canonical link for that post everywhere it shows a "view post" link. If you only return an id, Bangers uses that instead. If your response body is empty or doesn't parse, Bangers falls back to the delivery id. Any 2xx status code counts as a successful publish regardless of what the body contains. The reply fields are optional enrichment, not a requirement.

Verifying the signature

The signature is an HMAC-SHA256 over "<timestamp>.<raw body>", keyed with your signing secret, hex-encoded, and prefixed v1=. Verify it in constant time and reject anything outside a 5-minute timestamp window to guard against replay.

Node.js:

const crypto = require("crypto");

function verifyBangersWebhook(secret, signatureHeader, timestampHeader, rawBody) {
  if (!signatureHeader || !timestampHeader) return false;

  const timestamp = Number(timestampHeader);
  const now = Math.floor(Date.now() / 1000);
  if (!Number.isInteger(timestamp) || Math.abs(now - timestamp) > 5 * 60) {
    return false; // missing, malformed, or outside the 5-minute window
  }

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  const provided = signatureHeader.replace(/^v1=/, "");
  const expectedBuf = Buffer.from(expected, "hex");
  const providedBuf = Buffer.from(provided, "hex");

  return (
    expectedBuf.length === providedBuf.length &&
    crypto.timingSafeEqual(expectedBuf, providedBuf)
  );
}

// In your route handler, use the raw request body, not a re-serialized
// JSON.stringify(parsedBody), since re-encoding can change byte-for-byte
// formatting and break the comparison.
app.post("/hooks/bangers", express.raw({ type: "application/json" }), (req, res) => {
  const ok = verifyBangersWebhook(
    process.env.BANGERS_WEBHOOK_SECRET,
    req.header("X-Bangers-Signature"),
    req.header("X-Bangers-Timestamp"),
    req.body.toString("utf8"),
  );
  if (!ok) return res.sendStatus(401);

  const payload = JSON.parse(req.body.toString("utf8"));
  // handle payload.event === "ping" or "post.publish"
  res.sendStatus(200);
});

Python:

import hmac
import hashlib
import time

def verify_bangers_webhook(secret: str, signature_header: str, timestamp_header: str, raw_body: bytes) -> bool:
    if not signature_header or not timestamp_header:
        return False

    try:
        timestamp = int(timestamp_header)
    except ValueError:
        return False

    if abs(int(time.time()) - timestamp) > 5 * 60:
        return False  # outside the 5-minute window

    signed_payload = f"{timestamp}.".encode() + raw_body
    expected = hmac.new(secret.encode(), signed_payload, hashlib.sha256).hexdigest()
    provided = signature_header.removeprefix("v1=")

    return hmac.compare_digest(expected, provided)

# Use the raw request body bytes exactly as received, before any JSON parsing.

Retries and idempotency

If your endpoint returns a non-2xx status or times out, Bangers retries the delivery using the existing worker retry logic. Every retry for the same publish carries the same X-Bangers-Delivery id. Store it and de-duplicate on it, so a retried delivery doesn't create a second copy of the same article on your side. This is the same id you'll see echoed back if you look up the delivery from the Bangers side.

Failure behavior

A delivery that never succeeds (a non-2xx response, or no response within 20 seconds) is reported to you in Telegram within a minute of the failure, including the status Bangers received. You don't have to check a dashboard to find out publishing broke; you'll hear about it while it's still fresh enough to fix.

Security notes

  • HTTPS only. Bangers will not send to a plain HTTP endpoint.
  • Public hosts only. Localhost and private IP ranges are rejected at connect time (an SSRF guard), so an internal-only URL simply won't validate.
  • Redirects are not followed. A 3xx response is treated as a failed delivery, not a redirect target. Point the endpoint directly at its final URL.
  • The secret is never sent in the request. It's used only to compute the signature; Bangers doesn't transmit it on each call, so a network observer sees the signature, not the key.
  • Timeout is 20 seconds. Your endpoint should acknowledge and process asynchronously if publishing takes longer than that on your side.

How we use it

This blog is published through the same Custom webhook channel documented here. Every article you're reading went from an approved draft in Bangers, through this exact signed payload, to the endpoint that renders it, the same path available to any Bangers customer with a site of their own.

Build your business.
We'll help it show up.

Bangers is your AI social media team. You keep the direction and the final yes.

Get early access See how Bangers works