Webhook events
What yeetpost sends to your endpoint. These are outbound: you implement them.
A post was published
POSTpost.sent
Sent to every webhook of yours subscribed to post.sent when a post reaches its platform, whether it was posted immediately or on a schedule. Scheduling a post emits nothing on its own.
See the post.failed description for how to verify the signature; the rules are the same for both events.
{ "eventId": "8d3f2b7c-4e1a-4c6d-9b0f-1a2b3c4d5e6f", "event": "post.sent", "timestamp": "2026-08-26T09:24:12.004Z", "post": { "id": "6f1b0c8a-1f3d-4d3e-9a2b-2f5a1c9e7d10", "connectionSlug": "linkedin", "text": "shipped the new api docs today", "status": "sent", "url": "https://www.linkedin.com/feed/update/urn:li:share:1234567890", "scheduledFor": null, "createdAt": "2026-08-26T09:24:11.412Z", "error": null }}delivery headers
The event name, the same value as event in the body.
Id of the event, the same value as eventId in the body. Stable across every retry and every replay of one event, so it is the key to deduplicate on.
The same id as x-yeetpost-event-id, under the name it had before event ids.
t=<unix seconds>,v1=<hex hmac-sha256 of "<t>.<raw body>">, keyed by your webhook secret.
test when the post behind the delivery was made with a test key, live otherwise. Key your receiver off this to keep sandbox traffic out of its real handling.
body
Id of this event, the same value as the x-yeetpost-event-id header. Every retry and every replay of one event carries the same id, so a receiver that remembers what it has handled cannot handle it twice.
post.sent | post.failed | post.deleted | post.metrics | post.rescheduled | connection.created | connection.removed | connection.reconnect_required
When the event was queued.
Only present, and only ever true, on the synthetic delivery POST /webhooks/{webhookId}/test queues. Nothing was posted.
true
sent | failed
Same format as Timestamp, or null.
An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.
How many images the post carried. The ids are left out on purpose: read the bytes back with GET /media/{mediaId} if you need them.
How many replies went out under the head post.
Whether the first comment landed.
Why a post did not make it, or null when nothing went wrong.
Same vocabulary as the top-level error field.
Only on text_too_long: the part of the request that is too long, one of text, thread[<index>], firstComment or textByPlatform.<platformId>.
Your own labels for the post. At most 10, each 1 to 40 characters of lowercase letters, digits, dashes, underscores, colons, dots and slashes. Whitespace is trimmed, the tag is lowercased and repeats are dropped. Filter by them with GET /posts?tag=.
The profile the post's connection belongs to, null when it is one of your own accounts.
response 2XX
Any 2xx within 5 seconds counts as delivered. Anything else (or a timeout) is retried, up to 7 attempts in all, waiting 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and then 12 hours between them. After the seventh failure the delivery becomes a dead letter (status: dead) that is never retried, only replayed.
A post could not be published
POSTpost.failed
Sent to every webhook of yours subscribed to post.failed when a post is rejected by the platform, blocked by your plan limit, or fails on its way out. post.error carries the reason.
Verifying the signature. Each delivery carries x-yeetpost-signature: t=<unix seconds>,v1=<hex hmac>. Compute HMAC-SHA256("<t>.<raw request body>") with your webhook secret as the key and compare it to v1 with a constant-time comparison. Sign the raw body bytes, before any JSON parsing or re-serialisation. Also reject deliveries whose t is more than about 5 minutes away from your clock, so a captured delivery cannot be replayed later.
{ "eventId": "1c4e5f6a-7b8c-4d9e-8f0a-1b2c3d4e5f60", "event": "post.failed", "timestamp": "2026-08-26T09:24:12.004Z", "post": { "id": "0b7e4a52-9f0c-4a1c-8b3a-6d2f5b1c0e44", "connectionSlug": "x", "text": "shipped the new api docs today", "status": "failed", "url": null, "scheduledFor": null, "createdAt": "2026-08-26T09:24:11.412Z", "error": { "code": "platform_rejected", "message": "X: duplicate post was detected" } }}delivery headers
The event name, the same value as event in the body.
Id of the event, the same value as eventId in the body. Stable across every retry and every replay of one event, so it is the key to deduplicate on.
The same id as x-yeetpost-event-id, under the name it had before event ids.
t=<unix seconds>,v1=<hex hmac-sha256 of "<t>.<raw body>">, keyed by your webhook secret.
test when the post behind the delivery was made with a test key, live otherwise. Key your receiver off this to keep sandbox traffic out of its real handling.
body
Id of this event, the same value as the x-yeetpost-event-id header. Every retry and every replay of one event carries the same id, so a receiver that remembers what it has handled cannot handle it twice.
post.sent | post.failed | post.deleted | post.metrics | post.rescheduled | connection.created | connection.removed | connection.reconnect_required
When the event was queued.
Only present, and only ever true, on the synthetic delivery POST /webhooks/{webhookId}/test queues. Nothing was posted.
true
sent | failed
Same format as Timestamp, or null.
An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.
How many images the post carried. The ids are left out on purpose: read the bytes back with GET /media/{mediaId} if you need them.
How many replies went out under the head post.
Whether the first comment landed.
Why a post did not make it, or null when nothing went wrong.
Same vocabulary as the top-level error field.
Only on text_too_long: the part of the request that is too long, one of text, thread[<index>], firstComment or textByPlatform.<platformId>.
Your own labels for the post. At most 10, each 1 to 40 characters of lowercase letters, digits, dashes, underscores, colons, dots and slashes. Whitespace is trimmed, the tag is lowercased and repeats are dropped. Filter by them with GET /posts?tag=.
The profile the post's connection belongs to, null when it is one of your own accounts.
response 2XX
Any 2xx within 5 seconds counts as delivered. Anything else (or a timeout) is retried, up to 7 attempts in all, waiting 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and then 12 hours between them. After the seventh failure the delivery becomes a dead letter (status: dead) that is never retried, only replayed.
A post was deleted on its platform
POSTpost.deleted
Sent to every webhook of yours subscribed to post.deleted when DELETE /posts/{postId} takes a published post back down. Cancelling a scheduled post emits nothing: it was never published.
See the post.failed description for how to verify the signature; the rules are the same for every event.
{ "eventId": "2b7c8d3f-4e1a-4c6d-9b0f-1a2b3c4d5e6f", "event": "post.deleted", "timestamp": "2026-08-29T11:02:44.204Z", "post": { "id": "6f1b0c8a-1f3d-4d3e-9a2b-2f5a1c9e7d10", "connectionSlug": "linkedin", "platformId": "linkedin", "deletedAt": "2026-08-29T11:02:44.108Z" }}delivery headers
The event name, the same value as event in the body.
Id of the event, the same value as eventId in the body. Stable across every retry and every replay of one event, so it is the key to deduplicate on.
The same id as x-yeetpost-event-id, under the name it had before event ids.
t=<unix seconds>,v1=<hex hmac-sha256 of "<t>.<raw body>">, keyed by your webhook secret.
test when the post behind the delivery was made with a test key, live otherwise. Key your receiver off this to keep sandbox traffic out of its real handling.
body
Id of this event, the same value as the x-yeetpost-event-id header.
post.deleted
When the event was queued.
The platform the post came down on, such as x or mastodon.
An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.
The profile the post's connection belongs to, null when it is one of your own accounts.
response 2XX
Any 2xx within 5 seconds counts as delivered. Anything else (or a timeout) is retried, up to 7 attempts in all, waiting 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and then 12 hours between them. After the seventh failure the delivery becomes a dead letter (status: dead) that is never retried, only replayed.
A day's reading of a post's numbers
POSTpost.metrics
Sent to every webhook of yours subscribed to post.metrics on the first read of each UTC day, for as long as the post is being refreshed. That is one delivery per post per day for 30 days, not one per hourly read.
A number the platform does not report is null rather than 0.
See the post.failed description for how to verify the signature; the rules are the same for every event.
{ "eventId": "8d3f2b7c-4e1a-4c6d-9b0f-1a2b3c4d5e6f", "event": "post.metrics", "timestamp": "2026-08-30T10:00:00.120Z", "post": { "id": "6f1b0c8a-1f3d-4d3e-9a2b-2f5a1c9e7d10", "connectionSlug": "bluesky", "profileId": null, "platformId": "bluesky", "url": "https://bsky.app/profile/alex.bsky.social/post/3l2k" }, "metrics": { "fetchedAt": "2026-08-30T10:00:00.000Z", "impressions": null, "likes": 12, "reposts": 3, "replies": 1, "clicks": null }}delivery headers
The event name, the same value as event in the body.
Id of the event, the same value as eventId in the body. Stable across every retry and every replay of one event, so it is the key to deduplicate on.
The same id as x-yeetpost-event-id, under the name it had before event ids.
t=<unix seconds>,v1=<hex hmac-sha256 of "<t>.<raw body>">, keyed by your webhook secret.
test when the post behind the delivery was made with a test key, live otherwise. Key your receiver off this to keep sandbox traffic out of its real handling.
body
Id of this event, the same value as the x-yeetpost-event-id header.
post.metrics
When the event was queued.
The platform the numbers came from, such as x or mastodon.
The post's permalink.
The profile the post's connection belongs to, null when it is one of your own accounts.
One read of a post's numbers, as the platform reported them at that moment. Every counter is a running total, so the newest read is the post's numbers now.
An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.
How many times the post was shown. X and LinkedIn pages report it; Bluesky, Mastodon and personal LinkedIn profiles do not. Null means the platform does not report this number, which is not the same as zero.
Likes, favourites or reactions, whatever the platform calls them. Null means the platform does not report this number, which is not the same as zero.
Reposts, retweets or boosts. On Bluesky quotes are counted here too. LinkedIn does not report it. Null means the platform does not report this number, which is not the same as zero.
Replies or comments. Null means the platform does not report this number, which is not the same as zero.
Link clicks. LinkedIn pages report it; nobody else does. Null means the platform does not report this number, which is not the same as zero.
response 2XX
Any 2xx within 5 seconds counts as delivered. Anything else (or a timeout) is retried on the same schedule every other event uses.
The system moved a pending post's own time
POSTpost.rescheduled
Sent to every webhook of yours subscribed to post.rescheduled whenever a pending post's own time changes: a queued post missed its slot and was moved to the next free one (missed_slot), a fair-use deferral pushed it to the next UTC day (fair_use), a failed post was requeued by hand (requeued), a hand-scheduled post was spaced out behind a backlog draining on its connection (catch_up), or you moved it yourself with PATCH /posts/{postId} (edited).
See the post.failed description for how to verify the signature; the rules are the same for every event.
{ "eventId": "9e2c1a6b-3d0f-4b5c-8a9e-0f1a2b3c4d5e", "event": "post.rescheduled", "timestamp": "2026-08-30T10:00:00.120Z", "post": { "id": "6f1b0c8a-1f3d-4d3e-9a2b-2f5a1c9e7d10", "connectionSlug": "bluesky", "profileId": null, "scheduledFor": "2026-08-31T09:00:00.000Z", "reason": "missed_slot" }}delivery headers
The event name, the same value as event in the body.
Id of the event, the same value as eventId in the body. Stable across every retry and every replay of one event, so it is the key to deduplicate on.
The same id as x-yeetpost-event-id, under the name it had before event ids.
t=<unix seconds>,v1=<hex hmac-sha256 of "<t>.<raw body>">, keyed by your webhook secret.
test when the post behind the delivery was made with a test key, live otherwise. Key your receiver off this to keep sandbox traffic out of its real handling.
body
Id of this event, the same value as the x-yeetpost-event-id header.
post.rescheduled
When the event was queued.
The post's new time.
missed_slot is a queued post moved to its next free slot, fair_use is a fair-use deferral, requeued is a failed post moved back into the queue by hand, catch_up is a hand-scheduled post spaced out behind a backlog draining on its connection, edited is you moving it yourself with PATCH /posts/{postId}.
missed_slot | fair_use | requeued | catch_up | edited
The profile the post's connection belongs to, null when it is one of your own accounts.
response 2XX
Any 2xx within 5 seconds counts as delivered. Anything else (or a timeout) is retried on the same schedule every other event uses.
An account was connected
POSTconnection.created
Sent when an account is connected, whether it went into one of your profiles or into your own account. connection.id and connection.profileId together are the mapping to persist: the profile is your end customer, the connection is what you post to. Reconnecting an account that is already there is not a new connection and emits nothing. It also fires when a connection is moved into a profile with PATCH /connections/{connectionId}, paired with the connection.removed that names the profile it left.
{ "eventId": "1c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f", "event": "connection.created", "timestamp": "2026-08-29T09:24:12.004Z", "connection": { "id": "3f2a1b4c-5d6e-4f70-8a9b-0c1d2e3f4a5b", "profileId": "b2d1f0a3-5c6e-4f70-8a9b-0c1d2e3f4a5b", "connectSessionId": "7f3c9a10-2b4d-4e88-9f01-3a5b7c9d1e2f", "platformId": "linkedin", "slug": "linkedin" }}delivery headers
The event name, the same value as event in the body.
Id of the event, the same value as eventId in the body. Stable across every retry and every replay of one event, so it is the key to deduplicate on.
The same id as x-yeetpost-event-id, under the name it had before event ids.
t=<unix seconds>,v1=<hex hmac-sha256 of "<t>.<raw body>">, keyed by your webhook secret.
test when the post behind the delivery was made with a test key, live otherwise. Key your receiver off this to keep sandbox traffic out of its real handling.
body
The delivery's id, the same value as the x-yeetpost-event-id header. Dedupe on it.
connection.created | connection.removed | connection.reconnect_required
An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.
The profile the connection belongs to, null when it is one of your own accounts. This pair of ids is the mapping to persist.
The connect session the account came in through, null when the connection was made in the console. Lets you file the connection against the customer you made the link for without waiting for their browser to come back.
response 2XX
Any 2xx within 5 seconds counts as delivered. Anything else is retried on the same schedule the post events use.
An account was disconnected
POSTconnection.removed
Sent when a connection is disconnected, one at a time from the console or all at once by deleting the profile it sat in. The posts it already published stay readable; anything still waiting to go out is cancelled. It also fires when a connection is moved out of a profile with PATCH /connections/{connectionId}, paired with the connection.created that names the profile it arrived at.
{ "eventId": "1c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f", "event": "connection.removed", "timestamp": "2026-08-29T09:24:12.004Z", "connection": { "id": "3f2a1b4c-5d6e-4f70-8a9b-0c1d2e3f4a5b", "profileId": "b2d1f0a3-5c6e-4f70-8a9b-0c1d2e3f4a5b", "connectSessionId": null, "platformId": "linkedin", "slug": "linkedin" }}delivery headers
The event name, the same value as event in the body.
Id of the event, the same value as eventId in the body. Stable across every retry and every replay of one event, so it is the key to deduplicate on.
The same id as x-yeetpost-event-id, under the name it had before event ids.
t=<unix seconds>,v1=<hex hmac-sha256 of "<t>.<raw body>">, keyed by your webhook secret.
test when the post behind the delivery was made with a test key, live otherwise. Key your receiver off this to keep sandbox traffic out of its real handling.
body
The delivery's id, the same value as the x-yeetpost-event-id header. Dedupe on it.
connection.created | connection.removed | connection.reconnect_required
An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.
The profile the connection belongs to, null when it is one of your own accounts. This pair of ids is the mapping to persist.
The connect session the account came in through, null when the connection was made in the console. Lets you file the connection against the customer you made the link for without waiting for their browser to come back.
response 2XX
Any 2xx within 5 seconds counts as delivered. Anything else is retried on the same schedule the post events use.
A connection's login stopped working
POSTconnection.reconnect_required
Sent once, the moment yeetpost learns a connection's login has been refused or revoked: a token refresh was refused, or a post itself came back unauthorized. Nothing further fires while it stays that way; reconnecting it in the console clears the state, and the next refusal fires again.
This is not the only way GET /connections/{connectionId} answers status: "reconnect_required": a connection with no refresh token also turns reconnect_required once its stored login simply runs past loginExpiresAt, with nothing refused and no webhook sent, since nothing attempted to use it to notice.
{ "eventId": "1c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f", "event": "connection.reconnect_required", "timestamp": "2026-08-29T09:24:12.004Z", "connection": { "id": "3f2a1b4c-5d6e-4f70-8a9b-0c1d2e3f4a5b", "profileId": "b2d1f0a3-5c6e-4f70-8a9b-0c1d2e3f4a5b", "connectSessionId": null, "platformId": "linkedin", "slug": "linkedin" }}delivery headers
The event name, the same value as event in the body.
Id of the event, the same value as eventId in the body. Stable across every retry and every replay of one event, so it is the key to deduplicate on.
The same id as x-yeetpost-event-id, under the name it had before event ids.
t=<unix seconds>,v1=<hex hmac-sha256 of "<t>.<raw body>">, keyed by your webhook secret.
test when the post behind the delivery was made with a test key, live otherwise. Key your receiver off this to keep sandbox traffic out of its real handling.
body
The delivery's id, the same value as the x-yeetpost-event-id header. Dedupe on it.
connection.created | connection.removed | connection.reconnect_required
An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.
The profile the connection belongs to, null when it is one of your own accounts. This pair of ids is the mapping to persist.
The connect session the account came in through, null when the connection was made in the console. Lets you file the connection against the customer you made the link for without waiting for their browser to come back.
response 2XX
Any 2xx within 5 seconds counts as delivered. Anything else is retried on the same schedule the post events use.