Post Webhooks
Follow the publish lifecycle from scheduling to per-platform results, external sync, and TikTok URL resolution.
Subscribe to these events to follow a post from scheduling to every per-platform result without polling. Every delivery uses the shared envelope described in Webhooks.
Events
| Event | When |
|---|---|
post.scheduled | Post accepted with a future scheduledFor |
post.published | All destinations finished successfully |
post.failed | Post ended in a failed state |
post.partial | Some destinations published, others failed |
post.cancelled | Scheduled or queued post cancelled before publish |
post.platform.published | A destination went live on the platform |
post.platform.failed | A destination failed |
post.platform.deleted | Remote delete succeeded |
post.external.created | A native post appeared on a connected account |
post.external.updated | A tracked native post changed |
post.external.deleted | A tracked native post disappeared from the platform listing |
post.tiktok.url_resolved | A TikTok post's share URL became available |
How The Events Fit Together
A post moves through two layers of events. post.scheduled fires once when a post with a future scheduledFor is created and accepted. As each destination terminates, a per-destination post.platform.published or post.platform.failed streams in.
Once every destination has terminated, exactly one rollup fires on the status transition: post.published when all destinations succeeded, post.partial for a mixed result, post.failed when nothing is left live (failed, cancelled, and deleted destinations all count as not published), or post.cancelled when a scheduled or queued post is cancelled before publishing. Each rollup carries postId, status, and previousStatus, and fires once per status transition, so use the envelope id as an idempotency key.
On TikTok, post.tiktok.url_resolved trails the rollup: the share URL only exists once TikTok makes the post publicly viewable.
post.platform.deleted is different. It fires when you delete a destination through the API and the remote delete succeeds, not when background syncing notices a post is gone. That path emits post.external.deleted instead.
Payloads
post.scheduled:
{
"postId": "POST_ID",
"scheduledFor": "2026-07-16T12:00:00",
"timezone": "America/New_York",
"destinationIds": ["DEST_ID_1", "DEST_ID_2"]
}post.published (and post.failed / post.partial, same shape with a different status):
{
"postId": "POST_ID",
"status": "published",
"previousStatus": "processing"
}post.cancelled:
{
"postId": "POST_ID",
"previousStatus": "scheduled"
}post.platform.published:
{
"postId": "POST_ID",
"destinationId": "DEST_ID",
"accountId": "ACCOUNT_ID",
"platform": "x",
"externalPostId": "2077337724349026673",
"externalUrl": "https://x.com/i/web/status/2077337724349026673",
"publishedAt": "2026-07-15T12:00:01.000Z"
}post.platform.failed:
{
"postId": "POST_ID",
"destinationId": "DEST_ID",
"accountId": "ACCOUNT_ID",
"platform": "instagram",
"errorCode": "TOKEN_INVALID",
"errorMessage": "Access token is invalid or expired. Reconnect the account."
}post.platform.deleted:
{
"postId": "POST_ID",
"destinationId": "DEST_ID",
"accountId": "ACCOUNT_ID",
"platform": "x",
"externalPostId": "2077337724349026673"
}post.external.created and post.external.updated share this shape:
{
"accountId": "ACCOUNT_ID",
"platform": "x",
"post": {
"id": "EXT_ID",
"accountId": "ACCOUNT_ID",
"platform": "x",
"externalPostId": "1960956321831854280",
"externalUrl": null,
"mediaKind": "text",
"publishedAt": null,
"syncedAt": "<iso>",
"analyticsStatus": "ready"
}
}On a freshly connected account the first sync reports every existing native post as created. This one-time backfill is expected, so treat post.external.created as an idempotent upsert keyed on post.id.
post.external.deleted:
{
"accountId": "ACCOUNT_ID",
"platform": "x",
"deletedAt": "<iso>",
"post": {
"id": "EXT_ID",
"accountId": "ACCOUNT_ID",
"platform": "x",
"externalPostId": "1960956321831854280",
"externalUrl": null,
"mediaKind": "text",
"publishedAt": null,
"syncedAt": "<iso>",
"analyticsStatus": "ready"
}
}Deletion is only reported when a live listing provably covers the post: it was seen in a previous listing, its publishedAt falls inside the listing window, and its id is absent. Posts without a publishedAt are never flagged. Deleted rows disappear from sync results but keep their analytics.
post.tiktok.url_resolved:
{
"postId": "POST_ID",
"destinationId": "DEST_ID",
"accountId": "ACCOUNT_ID",
"platform": "tiktok",
"externalPostId": "7234567890123456789",
"externalUrl": "https://www.tiktok.com/@creator/video/7234567890123456789"
}Fires at most once per destination. No event fires when the destination already carried its share URL at publish time.