Guides
Webhooks
Get told when something happens instead of polling. Register an endpoint and PostLake POSTs an event to it when a post publishes, fails, or an account connects. Ideal for scheduled posts and async platforms.
Register an endpoint
Give PostLake a URL and the events you care about. You get back a WebhookEndpoint with a secret used to verify deliveries.
curl -X POST https://api.postlake.dev/v1/webhooks \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/hooks/postlake",
"events": ["post.published", "post.failed"]
}'Event types
| Event | Fires when |
|---|---|
post.published | Every target of a post has published. |
post.partial | Some targets published, some failed. |
post.failed | No target published. |
post.processing | A post moved to processing (async platforms). |
account.connected | A social account finished connecting. |
account.disconnected | A social account was disconnected. |
message.received | Someone messaged a connected Facebook or Instagram account. Meta pushes it to us and we pass it straight on. |
comment.received | Someone commented on one of your posts. |
mention.received | Someone mentioned one of your channels. |
Incoming messages, and why they arrive this way
Instagram and Facebook only let you reply within 24 hours of the other person's last message. Polling for new messages tells you roughly when that clock started; Meta telling us tells you exactly. That is why Meta messages come to you as an event, and it is also Meta's own guidance. X and Bluesky messages remain available through the unified inbox API, SDK, and MCP tools, but must be polled because those integrations do not push inbound message events to PostLake.
The payload carries replyBy, which is the moment the network stops accepting a reply, so nothing has to work it out:
{
"id": "evt_9f3c…",
"type": "message.received",
"createdAt": "2026-08-18T14:05:27.000Z",
"data": {
"account": "acc_9b2d…",
"platform": "instagram",
"handle": "postlake_",
"from": "1791754792026896",
"messageId": "aWdfZAG1f…",
"text": "Hey there",
"hasAttachments": false,
"receivedAt": "2026-08-18T14:05:27.000Z",
"replyBy": "2026-08-19T14:05:27.000Z"
}
}Reply with POST /v1/conversations/{id}/messages. A reply a person wrote can pass humanAgent: true, which extends the window to 7 days. Never set it on an automated reply: Meta grants that tag on the condition a human composed the message, and the penalty falls on the connected account.
Your own replies do not come back to you as events, so an agent watching this stream will not answer itself.
Comments and mentions work the same way, and for the same reason. Instagram has no way to poll for mentions at all, and polling for comments means asking every post you have ever made, over and over. So both are pushed: comment.received carries the comment id, the post it is on and the text, which is everything POST /v1/comments/{id}/replies needs to answer it.
The delivery
PostLake POSTs a JSON body with the event type and the affected resource (the full Post for post.* events), so you can act without a follow-up API call.
{
"type": "post.published",
"data": { "id": "post_a1b2c3", "state": "published", "targets": [ … ] }
}Verify the signature
Every delivery carries a postlake-signature header of the form t=<unix-seconds>,v1=<hex>. The v1 value is an HMAC-SHA256 of <t>.<raw-body> keyed with your endpoint secret. Verify it before trusting the payload. This proves the request really came from PostLake and (with a timestamp tolerance) blocks replays.
Verify the raw body, exactly as received. Don't parse and re-serialize the JSON first, or the bytes (and the signature) change.
With the SDK it's one call:
import { verifyWebhookSignature } from "postlake";
// in your handler. Pass the RAW request body
const ok = await verifyWebhookSignature(
endpointSecret, // the secret returned by webhooks.create()
rawBody, // exact bytes received
req.headers["postlake-signature"], // the signature header
{ toleranceSec: 300 }, // reject deliveries older than 5 min (optional)
);
if (!ok) return res.status(400).end();Or without the SDK, in plain Node:
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(secret, rawBody, header) {
const { t, v1 } = Object.fromEntries(header.split(",").map(kv => kv.split("=")));
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected), b = Buffer.from(v1);
return a.length === b.length && timingSafeEqual(a, b);
}Respond 2xx quickly once verified; failed deliveries are retried with backoff.
Managing endpoints
List your endpoints with GET /v1/webhooks and remove one with DELETE /v1/webhooks/{id}. See the API reference for the exact shapes.