Events
Every webhook event FoPost can send, when it fires, and its payload.
Nineteen events, in six families. Subscribe to delivery.* when you want per-account granularity, post.* when you only care about the overall outcome. Subscribing to both means two notifications for the same publish, which is fine as long as your handler expects it.
Posts
| Event | Fires when |
|---|---|
post.scheduled | A post was put on the calendar, by hand, by an approval, by a bulk import or by an automation. Rescheduling fires it again |
post.published | Every account on the post published successfully |
post.partially_failed | Some accounts published, some did not |
post.failed | No account published (failed and cancelled deliveries both count) |
post.deleted | A post was deleted. Any delivery still pending was cancelled with it |
The three outcome events fire once per post, when every account has a final status. data carries the post and its status:
{ "postId": "7d2b8c11-…", "status": "published" }post.scheduled adds scheduleAt; post.deleted carries only postId.
Delivery outcomes
One event per account on the post.
| Event | Fires when |
|---|---|
delivery.published | One account published. Fires once per account |
delivery.failed | One account will not publish, after its retries, or immediately when the failure is final, such as expired credentials or an error the platform will never accept |
delivery.delayed | One delivery was held by a platform rate limit and will retry at retryAt |
delivery.published data:
{ "postId": "…", "postAccountId": "…", "accountId": "…", "platform": "linkedin", "externalUrl": "https://…" }externalUrl is the live post when the platform reports one, and null when it does not.
delivery.failed swaps externalUrl for errorCode and errorMessage; delivery.delayed carries reason: "platform_rate_limit", a message, and retryAt. Ordinary retry backoff between attempts does not emit delivery.delayed; only a platform rate limit does.
Accounts
| Event | Fires when |
|---|---|
account.connected | A social account was connected to the workspace |
account.disconnected | A social account was disconnected |
account.health_changed | A connected account's credential health changed: degraded, expired, revoked, or recovered |
account.connected and account.disconnected carry the account:
{ "accountId": "…", "platform": "instagram", "username": "yourbrand" }account.health_changed adds the transition:
{ "accountId": "…", "platform": "instagram", "username": "yourbrand", "previousStatus": "healthy", "newStatus": "expired" }This is the one worth wiring even if you skip the rest. It is how you learn a token died before a scheduled post silently fails. It fires on recovery too, so you can close the alert it opened. When the change comes from a failed token refresh rather than a health check, the payload carries a reason instead of a username.
Articles
| Event | Fires when |
|---|---|
article.published | A blog article was published to its target: a connected account, your website, or the product blog |
Unlike the other events, data carries the whole article, so a website can store it without a follow-up GET:
{
"id": "b41e7c05-9d38-4a2f-8e60-1c7d5b3a9f24",
"title": "A Marathon Training Plan for First-Time Runners",
"slug": "marathon-training-plan-for-first-time-runners",
"excerpt": "Sixteen weeks, three runs a week, and one long run that grows slowly.",
"metaTitle": "Marathon Training Plan for Beginners (16 Weeks)",
"metaDescription": "A sixteen-week marathon plan for first-time runners.",
"bodyMarkdown": "## Why sixteen weeks\n\n...",
"bodyHtml": "<h2>Why sixteen weeks</h2><p>...</p>",
"featuredImage": { "mediaId": "6b0e9a4c-…", "url": "https://…", "alt": "A runner lacing shoes at dawn" },
"images": [{ "mediaId": "9c2f7b31-…", "url": "https://…", "alt": "A weekly mileage chart", "position": 2 }],
"tags": ["marathon", "training"],
"keyword": "marathon training plan for beginners",
"wordCount": 1540,
"publishedAt": "2026-10-14T07:00:00.000Z",
"publishedUrl": "https://yourbrand.com/blog/marathon-training-plan-for-first-time-runners"
}excerpt, metaTitle, metaDescription, featuredImage, keyword and publishedUrl are null when there is nothing to put in them. position on an in-body image is the index of the outline heading it sits under; the featured image has none. bodyMarkdown references the same image URLs inline.
Image URLs expire after 6 hours. Download each image and store your own copy when the event arrives; a URL kept past that window stops working.
The Website target POSTs this same envelope straight to the URL in your blog settings, signed with its own secret, and sends article.test from the settings page. The receiving guide covers that receiver.
Inbox
One event per new inbound item, as the inbox poll picks it up. Your own replies never fire one.
| Event | Fires when |
|---|---|
inbox.comment_received | Someone commented on one of your posts |
inbox.mention_received | Someone mentioned one of your accounts |
inbox.message_received | A direct message arrived |
inbox.review_received | A review or Page recommendation arrived |
inbox.reply_sent | A reply you sent from the inbox reached the platform |
{ "itemId": "…", "type": "comment", "platform": "instagram", "accountId": "…", "receivedAt": "2026-09-01T09:00:12.000Z" }inbox.reply_sent carries the same ids plus replyId (the platform's id for your reply) and sentAt instead of receivedAt.
The payload is ids only. Fetch the item from the inbox for its text and author, so nothing a customer wrote sits in your webhook logs. Building a bot on inbox.message_received? The chat adapter does the verify-and-fetch half for you.
Automations
| Event | Fires when |
|---|---|
automation.triggered | A run started |
automation.completed | A run finished every step |
automation.failed | A run stopped on an error |
{ "automationId": "…", "runId": 4821, "totalSteps": 3 }automation.triggered carries automationId and runId; automation.completed adds totalSteps; automation.failed adds error.
The envelope
Every delivery is the same three-key body:
{ "event": "<name>", "data": { … }, "timestamp": "<ISO 8601>" }timestamp is when the event was queued. The delivery id lives in the X-FoPost-Delivery header. Deduplicate on that, since a retried delivery repeats the identical body. Ids in data are the same public ids the API uses, so a webhook payload can be followed with a GET.
Next
Related documentation
- Webhooks
Hear about publishing outcomes and account health without polling.
- Verifying Signatures
Prove a delivery came from FoPost before acting on it.
- Retries and Failures
Timeouts, retry backoff, auto-disable, and test deliveries.
- Blog
A keyword plan, articles written in the background, a review window, and a target they publish to.
- Receiving Blog Articles
Publish FoPost articles on your own site, whatever it is built with.
- Inbox
Comments, mentions and reviews from every connected account, in one list.
- Chat Adapter
Plug the FoPost inbox into a chatbot framework as one send/receive channel.
- API Overview
Base URL, envelopes, pagination, and errors for the FoPost REST API.