Webhooks
Outgoing webhooks exist: subscribe an endpoint to a handful of post events and Postlyra sends it a signed HTTP request when one happens. Set them up in Studio, under Settings, then the API tab. Only the brand owner can create or edit one.
Events
| Event | Fires when |
|---|---|
post.published | A scheduled publication goes live |
post.failed | A scheduled publication fails, after retries are exhausted |
post.expired | A scheduled publication's time passed without approval |
post.approved | A post is approved, from Studio, MCP, the mobile app, or a public share link |
post.commented | Someone comments on a post's review thread |
An endpoint picks which of these it wants; it never receives one it did not subscribe to.
Payload
Every event ships the same envelope:
{
"post_id": 123,
"status": "published",
"publication": { "id": 456, "platform": "linkedin", "external_id": "urn:li:...", "permalink": "https://..." },
"brand_id": 7,
"occurred_at": "2026-08-25T09:00:03+00:00"
}publication appears only on post.published, post.failed and post.expired. post.approved carries approved_via instead of publication. post.commented carries comment_id, kind (comment, approval or change_request) and body instead.
Verifying a delivery
Every request carries:
X-Postlyra-Signature: t=1756112345,v1=8f3c2a...
X-Postlyra-Event: post.publishedv1 is HMAC-SHA256("{t}.{raw request body}", <your endpoint's secret>). The secret is shown once, when you create the endpoint.
const crypto = require("crypto")
function verify(rawBody, signatureHeader, secret) {
const [tPart, v1Part] = signatureHeader.split(",")
const timestamp = tPart.split("=")[1]
const signature = v1Part.split("=")[1]
const expected = crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex")
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
}Use the exact raw bytes received, before any JSON parsing, or the signature will not match.
Delivery, retries, and auto-disable
- Three attempts, backed off 10 seconds, then 60, then 5 minutes.
- 10 consecutive deliveries that exhaust all their retries disable the endpoint automatically. You get one notification, not one per attempt. Fix it and re-enable it in Studio, which resets the streak.
- The delivery log, on the same Settings page, keyset-lists every attempt with its status, response code and error.
- Send test fires a real signed delivery (
event: "test") through the same code path a live event uses, so a passing test proves your signature verification actually works.
Inbound callbacks are a separate thing
Postlyra also receives callbacks from platforms it integrates with: Meta deauthorization and data-deletion requests, Instagram events, each platform's OAuth callback, and Stripe billing events. These are not something you subscribe to or call. They live on a separate host and verify signatures from the calling platform, documented here only so nobody wires an integration against them by mistake.
| Endpoint | Who calls it |
|---|---|
POST /webhooks/meta/deauthorize | Meta, when a user removes the app |
POST /webhooks/meta/data-deletion | Meta, on a data deletion request |
GET and POST /webhooks/meta/instagram | Meta, delivering Instagram events such as DMs and comments |
GET /oauth/{platform}/callback | The platform, finishing an OAuth connect |
| Stripe's webhook | Stripe, on billing events |
Polling for anything else
The five post events above are the whole outgoing list. For anything not on it, poll instead.
| What you want | Endpoint | Sensible interval |
|---|---|---|
| New ideas in the queue | list_ideas on MCP | Minutes |
| Whether a plan changed | GET /subscription/status | Hourly |
| Remaining allowances | GET /me/entitlements | Hourly |
Metrics sync once a night, so polling them more often than daily reads the same numbers repeatedly. See Rate limits.