Webhooks

PostWire calls your server the moment a post goes out or fails on a network, and when a connected account has to be signed in again. No polling. Every plan, including Free.

One event per networkA post to three networks that worked on two sends two post.published and one post.failed.
SignedHMAC-SHA256 with your endpoint's secret, in the same format Stripe uses.
Retried for two days9 tries with backoff. Every try is in the delivery log, so nothing is lost silently.
Failures tooFailures and expired logins are sent too, not only successes, with a machine-readable code.

Events

EventWhen
post.publishedA network accepted the post, from the API, the dashboard, n8n, MCP or the scheduler. For an Instagram video that is still processing, or a large YouTube/TikTok upload that continues on the next run, the event is sent when it finishes, not before.
post.failedA network refused the post or it could not be sent: not connected, media missing or unreachable, a plan limit, or the network's own error. status: "unknown" with code: "timed_out" means the network did not answer in time. Check the network before you post again.
connection.reauth_requiredA network said its sign-in expired or was revoked. LinkedIn sign-ins expire about every 60 days. The event includes reconnect_url.
webhook.testOnly when you ask for it with the test call below.

On the Free plan, networks over the 2 per post are held: they were never sent, so they send no event.

Add an endpoint

In the dashboard, go to API & MCP and then Webhooks. Or use the API. The response contains the signing secret, and it is shown only once.

curl -X POST https://postwire.io/api/webhooks \
  -H "Authorization: Bearer $POSTWIRE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/hooks/postwire","events":["post.published","post.failed","connection.reauth_required"]}'

# → 201 { "webhook": { "id": "…", "url": "…", "events": […] }, "secret": "whsec_…" }

curl -X POST https://postwire.io/api/webhooks/<id>/test -H "Authorization: Bearer $POSTWIRE_API_KEY"
curl https://postwire.io/api/webhooks/<id>/deliveries -H "Authorization: Bearer $POSTWIRE_API_KEY"
curl -X DELETE https://postwire.io/api/webhooks/<id> -H "Authorization: Bearer $POSTWIRE_API_KEY"

Rules: the URL must use https and point to a public address. PostWire does not follow redirects. You can add up to 5 endpoints per account. If you leave out events, the endpoint gets all three.

What arrives

POST /hooks/postwire
Content-Type: application/json
PostWire-Event: post.published
PostWire-Delivery: 3f6c2a0e-…            (the same on every retry: dedupe on it)
PostWire-Signature: t=1759489200,v1=5d1c…

{
  "id": "evt_9b0f…",
  "type": "post.published",
  "created_at": "2026-10-03T14:00:02.118Z",
  "data": {
    "platform": "youtube",
    "status": "published",
    "post_id": "dQw4w9WgXcQ",
    "url": "https://youtube.com/shorts/dQw4w9WgXcQ",
    "error": null,
    "code": null,
    "account": { "platform": "youtube", "handle": "@acme", "brand_id": "…", "brand": "Acme" },
    "brand_id": "…",
    "source": "schedule",
    "schedule_id": "8a1d…",
    "idempotency_key": null,
    "label": "Monday launch"
  }
}

source is api for POST /api/post, which the dashboard, n8n and MCP also use, and schedule for a scheduled post. If you send an idempotency_key with a publish, it comes back in the event, so you can match the event to your request. The full schema is WebhookEvent in openapi.json.

Verify the signature

Compute HMAC-SHA256 with your secret over <t>.<raw body> and compare the result with v1. Use the raw bytes of the body, not re-serialized JSON. Reject a t that is more than 5 minutes old.

// Node.js (Express)
import crypto from "node:crypto";
app.post("/hooks/postwire", express.raw({ type: "application/json" }), (req, res) => {
  const parts = Object.fromEntries(req.get("PostWire-Signature").split(",").map((p) => p.split("=")));
  const want = crypto.createHmac("sha256", process.env.POSTWIRE_WEBHOOK_SECRET).update(`${parts.t}.${req.body}`).digest("hex");
  const a = Buffer.from(want), b = Buffer.from(parts.v1 || "");
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  if (!fresh || a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(400);
  const event = JSON.parse(req.body);
  // … handle event.type, dedupe on req.get("PostWire-Delivery") …
  res.sendStatus(200);
});
# Python (Flask)
import hmac, hashlib, time, os
@app.post("/hooks/postwire")
def postwire_hook():
    parts = dict(p.split("=", 1) for p in request.headers["PostWire-Signature"].split(","))
    body = request.get_data()
    want = hmac.new(os.environ["POSTWIRE_WEBHOOK_SECRET"].encode(), f"{parts['t']}.".encode() + body, hashlib.sha256).hexdigest()
    if abs(time.time() - int(parts["t"])) > 300 or not hmac.compare_digest(want, parts.get("v1", "")):
        return "", 400
    event = request.get_json()
    return "", 200

Delivery and retries

With n8n, Make or Zapier

Add a Webhook trigger node (n8n) or a Custom webhook (Make, Zapier). Copy its production URL, add it as an endpoint, then send a test. Then branch on {{ $json.body.type }}. For example, send post.failed to Slack and connection.reauth_required to the client who has to sign in again.

Related: live status and delivery rates by network · changelog · OpenAPI · MCP server