Webhooks

Add an endpoint in a project's Settings → Developers and choose its events. Postwase sends a POST with a JSON body for each event, and shows you the signing secret once.

Events

post.publishedEvery channel of a post published.
post.partially_publishedSome channels published, others failed.
post.failedNo channel published.
channel.needs_reauthA connected account lost access and must be reconnected.
pingSent by “Send test”.
post.published
{
  "id": "evt_6f1c…",
  "type": "post.published",
  "createdAt": "2026-11-02T02:00:04.120Z",
  "projectId": "…",
  "data": {
    "postId": "…",
    "status": "published",
    "targets": [
      { "id": "…", "channelId": "…", "platform": "facebook", "status": "published", "permalink": "https://facebook.com/…", "error": null }
    ]
  }
}

Delivery

  • Reply with any 2xx within 10 seconds. Do slow work after replying.
  • Failures are retried after 1 min, 5 min, 30 min, 2 h, 6 h and 24 h, then marked failed. The endpoint's log in Settings shows each attempt.
  • Deliveries can repeat: deduplicate on the event id (also in the Postwase-Delivery header per endpoint).
  • Redirects are not followed; use the final URL.

Verify the signature

Postwase-Signature: t=<unix seconds>,v1=<hex>, where the hex is the HMAC-SHA256 of t + "." + raw body keyed with the endpoint secret. Check it against the raw body (before JSON parsing) and reject old timestamps.

Node.js
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyPostwase(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > toleranceSec) return false;
  const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  return expected.length === parts.v1?.length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
Python
import hmac, hashlib, time

def verify_postwase(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    if abs(time.time() - int(parts["t"])) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))