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.published | Every channel of a post published. |
post.partially_published | Some channels published, others failed. |
post.failed | No channel published. |
channel.needs_reauth | A connected account lost access and must be reconnected. |
ping | Sent 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
2xxwithin 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 thePostwase-Deliveryheader 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", ""))