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
| type | When |
|---|---|
post.published | Every target of a post published. |
post.partial | Some targets published and some failed. |
post.failed | No target published. |
account.connected | An account was connected. |
account.needs_reauth | An account's access expired or was revoked; it must be reconnected before it can publish. |
account.disconnected | An account was removed. |
live.key_set | A YouTube channel's stream key was set or changed. Carries the RTMP server and key (stored encrypted, masked in the delivery log). |
live.broadcast_scheduled | A 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.test | Sent 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.
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.
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={
"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:
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:
| Attempt | When |
|---|---|
| 1 | Immediately |
| 2 | 30 seconds later |
| 3 | 2 minutes later |
| 4 | 10 minutes later |
| 5 | 1 hour later |
| 6 | 3 hours later |
| 7 | 6 hours later |
| 8 | 12 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
- POST
/webhook-endpoints/{id}/testsends awebhook.testevent now and tells you how your server answered. It is never retried and never counts toward auto-disable. - GET
/webhook-endpoints/{id}/deliverieslists deliveries, newest first, with status codes, timings, the first 500 characters of your response and the exact body we signed. Kept 30 days. - POST
/webhook-endpoints/{id}/deliveries/{deliveryId}/retrysends any delivery again with a fresh set of attempts. - Leaked secret? PATCH
/webhook-endpoints/{id}with{ "rotateSecret": true }issues a new one; the old one stops working immediately.