Guides

Webhooks

Instead of polling, give us an https URL. We POST a signed event to it when a post lands, partly lands or fails, and when an account connects, disconnects or needs reconnecting.

Events

typeWhen
post.publishedEvery target of a post published.
post.partialSome targets published and some failed.
post.failedNo target published.
account.connectedAn account was connected.
account.needs_reauthAn account's access expired or was revoked; it must be reconnected before it can publish.
account.disconnectedAn account was removed.
live.key_setA YouTube channel's stream key was set or changed. Carries the RTMP server and key (stored encrypted, masked in the delivery log).
live.broadcast_scheduledA YouTube Live broadcast was created on a channel. Carries the server and key it streams to, and whether that is the channel's usual key.
webhook.testSent only by the test endpoint.

Create an endpoint

Needs the webhooks:manage scope and an org admin's key that isn't limited to account groups. An organisation can have up to 10 endpoints. Save the secret from the response — it is shown only now. You can also manage endpoints in the studio.

Request
curl -X POST "https://dashboard.lifebliss.app/api/v1/webhook-endpoints" \
  -H "Authorization: Bearer $PS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://app.example.org/hooks/publishing",
    "events": ["post.published", "post.partial", "post.failed", "account.needs_reauth"],
    "description": "Production listener"
  }'

# 201 → { "endpoint": { "id": "…", … }, "secret": "whsec_…" }   (shown only once)

What we send

A JSON POST signed per Standard Webhooks. For post.* events, data holds the post, its progress and up to 100 targets; for account.* events it holds the account's id, platform, display name and username.

Headers
POST /hooks/publishing HTTP/1.1
Content-Type: application/json
User-Agent: KailasaPublishingStudio-Webhooks/1.0
webhook-id: evt_…
webhook-timestamp: 1793563804
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
Body
{
  "id": "evt_…",
  "type": "post.partial",
  "createdAt": "2026-11-01T13:30:04Z",
  "data": {
    "post": { "id": "c1d2…", "status": "partial" },
    "progress": { "total": 2, "published": 1, "failed": 1 },
    "targets": [ … ],
    "targetsTruncated": false
  }
}

Verify the signature

The signature is v1, followed by the base64 HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{body}, keyed with the base64-decoded part of your secret after whsec_. Any Standard Webhooks library can verify it; this is the whole check in plain Node.js:

verify.mjs
import { createHmac, timingSafeEqual } from "node:crypto";

// Use the raw request body, exactly as received — not re-serialised JSON.
export function verify(secret, headers, rawBody) {
  const id = headers["webhook-id"];
  const ts = Number(headers["webhook-timestamp"]);
  const sent = headers["webhook-signature"] ?? "";

  // Reject anything older or newer than five minutes (replays).
  if (!id || Math.abs(Date.now() / 1000 - ts) > 300) return false;

  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = "v1," + createHmac("sha256", key)
    .update(`${id}.${ts}.${rawBody}`)
    .digest("base64");

  // The header may hold several space-separated signatures.
  return sent.split(" ").some((sig) =>
    sig.length === expected.length && timingSafeEqual(Buffer.from(sig), Buffer.from(expected)));
}

Respond and deduplicate

  • Answer with any 2xx within 5 seconds. Anything else, or no answer, counts as a failure. Do slow work after you respond.
  • A retried event keeps the same id. Store the ids you have handled and ignore repeats.
  • Events can arrive out of order. When order matters, read the current state with GET/posts/{id}.

Retries and auto-disable

A failed delivery is tried up to eight times:

AttemptWhen
1Immediately
230 seconds later
32 minutes later
410 minutes later
51 hour later
63 hours later
76 hours later
812 hours later — the last attempt, about 22 hours after the first

If five deliveries in a row fail every attempt, the endpoint is disabled and its disabledReason says why. Re-enable it with PATCH/webhook-endpoints/{id} and { "enabled": true } — queued deliveries resume. Deliveries waiting on an endpoint that stays disabled are dropped after 7 days.

Testing and debugging