Skip to content
yeetpostyeetpost

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.

delivery
{  "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

x-yeetpost-eventstringrequired

The event name, the same value as event in the body.

x-yeetpost-event-idstring <uuid>required

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.

x-yeetpost-deliverystring <uuid>required

The same id as x-yeetpost-event-id, under the name it had before event ids.

x-yeetpost-signaturestring, matches ^t=[0-9]+,v1=[0-9a-f]{64}$required

t=<unix seconds>,v1=<hex hmac-sha256 of "<t>.<raw body>">, keyed by your webhook secret.

x-yeetpost-modestringrequired

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

eventIdstring <uuid>required

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.

eventstringrequired

post.sent | post.failed | post.deleted | post.metrics | post.rescheduled | connection.created | connection.removed | connection.reconnect_required

timestampstring <date-time>required

When the event was queued.

testbooleanoptional

Only present, and only ever true, on the synthetic delivery POST /webhooks/{webhookId}/test queues. Nothing was posted.

true

postobjectrequired
post.idstring <uuid>required
post.connectionSlugstringrequired
post.textstringrequired
post.statusstringrequired

sent | failed

post.urlstring | nullrequired
post.scheduledForstring <date-time> | nullrequired

Same format as Timestamp, or null.

post.createdAtstring <date-time>required

An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.

post.mediaCountintegerrequired

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.

post.threadCountintegerrequired

How many replies went out under the head post.

post.hasFirstCommentbooleanrequired

Whether the first comment landed.

post.errorobject | nullrequired

Why a post did not make it, or null when nothing went wrong.

post.error.codestringrequired

Same vocabulary as the top-level error field.

post.error.messagestringrequired
post.error.fieldstringoptional

Only on text_too_long: the part of the request that is too long, one of text, thread[<index>], firstComment or textByPlatform.<platformId>.

post.tagsstring[], at most 10 itemsrequired

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=.

post.profileIdstring <uuid> | nullrequired

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.

delivery
{  "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

x-yeetpost-eventstringrequired

The event name, the same value as event in the body.

x-yeetpost-event-idstring <uuid>required

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.

x-yeetpost-deliverystring <uuid>required

The same id as x-yeetpost-event-id, under the name it had before event ids.

x-yeetpost-signaturestring, matches ^t=[0-9]+,v1=[0-9a-f]{64}$required

t=<unix seconds>,v1=<hex hmac-sha256 of "<t>.<raw body>">, keyed by your webhook secret.

x-yeetpost-modestringrequired

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

eventIdstring <uuid>required

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.

eventstringrequired

post.sent | post.failed | post.deleted | post.metrics | post.rescheduled | connection.created | connection.removed | connection.reconnect_required

timestampstring <date-time>required

When the event was queued.

testbooleanoptional

Only present, and only ever true, on the synthetic delivery POST /webhooks/{webhookId}/test queues. Nothing was posted.

true

postobjectrequired
post.idstring <uuid>required
post.connectionSlugstringrequired
post.textstringrequired
post.statusstringrequired

sent | failed

post.urlstring | nullrequired
post.scheduledForstring <date-time> | nullrequired

Same format as Timestamp, or null.

post.createdAtstring <date-time>required

An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.

post.mediaCountintegerrequired

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.

post.threadCountintegerrequired

How many replies went out under the head post.

post.hasFirstCommentbooleanrequired

Whether the first comment landed.

post.errorobject | nullrequired

Why a post did not make it, or null when nothing went wrong.

post.error.codestringrequired

Same vocabulary as the top-level error field.

post.error.messagestringrequired
post.error.fieldstringoptional

Only on text_too_long: the part of the request that is too long, one of text, thread[<index>], firstComment or textByPlatform.<platformId>.

post.tagsstring[], at most 10 itemsrequired

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=.

post.profileIdstring <uuid> | nullrequired

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.

delivery
{  "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

x-yeetpost-eventstringrequired

The event name, the same value as event in the body.

x-yeetpost-event-idstring <uuid>required

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.

x-yeetpost-deliverystring <uuid>required

The same id as x-yeetpost-event-id, under the name it had before event ids.

x-yeetpost-signaturestring, matches ^t=[0-9]+,v1=[0-9a-f]{64}$required

t=<unix seconds>,v1=<hex hmac-sha256 of "<t>.<raw body>">, keyed by your webhook secret.

x-yeetpost-modestringrequired

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

eventIdstring <uuid>required

Id of this event, the same value as the x-yeetpost-event-id header.

eventstringrequired

post.deleted

timestampstring <date-time>required

When the event was queued.

postobjectrequired
post.idstring <uuid>required
post.connectionSlugstringrequired
post.platformIdstringrequired

The platform the post came down on, such as x or mastodon.

post.deletedAtstring <date-time>required

An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.

post.profileIdstring <uuid> | nullrequired

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.

delivery
{  "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

x-yeetpost-eventstringrequired

The event name, the same value as event in the body.

x-yeetpost-event-idstring <uuid>required

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.

x-yeetpost-deliverystring <uuid>required

The same id as x-yeetpost-event-id, under the name it had before event ids.

x-yeetpost-signaturestring, matches ^t=[0-9]+,v1=[0-9a-f]{64}$required

t=<unix seconds>,v1=<hex hmac-sha256 of "<t>.<raw body>">, keyed by your webhook secret.

x-yeetpost-modestringrequired

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

eventIdstring <uuid>required

Id of this event, the same value as the x-yeetpost-event-id header.

eventstringrequired

post.metrics

timestampstring <date-time>required

When the event was queued.

postobjectrequired
post.idstring <uuid>required
post.connectionSlugstringrequired
post.platformIdstringrequired

The platform the numbers came from, such as x or mastodon.

post.urlstring | nullrequired

The post's permalink.

post.profileIdstring <uuid> | nullrequired

The profile the post's connection belongs to, null when it is one of your own accounts.

metricsobjectrequired

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.

metrics.fetchedAtstring <date-time>required

An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.

metrics.impressionsinteger | nullrequired

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.

metrics.likesinteger | nullrequired

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.

metrics.repostsinteger | nullrequired

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.

metrics.repliesinteger | nullrequired

Replies or comments. Null means the platform does not report this number, which is not the same as zero.

metrics.clicksinteger | nullrequired

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.

delivery
{  "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

x-yeetpost-eventstringrequired

The event name, the same value as event in the body.

x-yeetpost-event-idstring <uuid>required

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.

x-yeetpost-deliverystring <uuid>required

The same id as x-yeetpost-event-id, under the name it had before event ids.

x-yeetpost-signaturestring, matches ^t=[0-9]+,v1=[0-9a-f]{64}$required

t=<unix seconds>,v1=<hex hmac-sha256 of "<t>.<raw body>">, keyed by your webhook secret.

x-yeetpost-modestringrequired

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

eventIdstring <uuid>required

Id of this event, the same value as the x-yeetpost-event-id header.

eventstringrequired

post.rescheduled

timestampstring <date-time>required

When the event was queued.

postobjectrequired
post.idstring <uuid>required
post.connectionSlugstringrequired
post.scheduledForstring <date-time>required

The post's new time.

post.reasonstringrequired

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

post.profileIdstring <uuid> | nullrequired

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.

delivery
{  "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

x-yeetpost-eventstringrequired

The event name, the same value as event in the body.

x-yeetpost-event-idstring <uuid>required

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.

x-yeetpost-deliverystring <uuid>required

The same id as x-yeetpost-event-id, under the name it had before event ids.

x-yeetpost-signaturestring, matches ^t=[0-9]+,v1=[0-9a-f]{64}$required

t=<unix seconds>,v1=<hex hmac-sha256 of "<t>.<raw body>">, keyed by your webhook secret.

x-yeetpost-modestringrequired

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

eventIdstring <uuid>required

The delivery's id, the same value as the x-yeetpost-event-id header. Dedupe on it.

eventstringrequired

connection.created | connection.removed | connection.reconnect_required

timestampstring <date-time>required

An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.

connectionobjectrequired
connection.idstring <uuid>required
connection.profileIdstring <uuid> | nullrequired

The profile the connection belongs to, null when it is one of your own accounts. This pair of ids is the mapping to persist.

connection.connectSessionIdstring <uuid> | nullrequired

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.

connection.platformIdstringrequired
connection.slugstringrequired

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.

delivery
{  "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

x-yeetpost-eventstringrequired

The event name, the same value as event in the body.

x-yeetpost-event-idstring <uuid>required

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.

x-yeetpost-deliverystring <uuid>required

The same id as x-yeetpost-event-id, under the name it had before event ids.

x-yeetpost-signaturestring, matches ^t=[0-9]+,v1=[0-9a-f]{64}$required

t=<unix seconds>,v1=<hex hmac-sha256 of "<t>.<raw body>">, keyed by your webhook secret.

x-yeetpost-modestringrequired

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

eventIdstring <uuid>required

The delivery's id, the same value as the x-yeetpost-event-id header. Dedupe on it.

eventstringrequired

connection.created | connection.removed | connection.reconnect_required

timestampstring <date-time>required

An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.

connectionobjectrequired
connection.idstring <uuid>required
connection.profileIdstring <uuid> | nullrequired

The profile the connection belongs to, null when it is one of your own accounts. This pair of ids is the mapping to persist.

connection.connectSessionIdstring <uuid> | nullrequired

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.

connection.platformIdstringrequired
connection.slugstringrequired

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.

delivery
{  "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

x-yeetpost-eventstringrequired

The event name, the same value as event in the body.

x-yeetpost-event-idstring <uuid>required

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.

x-yeetpost-deliverystring <uuid>required

The same id as x-yeetpost-event-id, under the name it had before event ids.

x-yeetpost-signaturestring, matches ^t=[0-9]+,v1=[0-9a-f]{64}$required

t=<unix seconds>,v1=<hex hmac-sha256 of "<t>.<raw body>">, keyed by your webhook secret.

x-yeetpost-modestringrequired

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

eventIdstring <uuid>required

The delivery's id, the same value as the x-yeetpost-event-id header. Dedupe on it.

eventstringrequired

connection.created | connection.removed | connection.reconnect_required

timestampstring <date-time>required

An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.

connectionobjectrequired
connection.idstring <uuid>required
connection.profileIdstring <uuid> | nullrequired

The profile the connection belongs to, null when it is one of your own accounts. This pair of ids is the mapping to persist.

connection.connectSessionIdstring <uuid> | nullrequired

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.

connection.platformIdstringrequired
connection.slugstringrequired

response 2XX

Any 2xx within 5 seconds counts as delivered. Anything else is retried on the same schedule the post events use.

The signature recipe, the delivery log and a working Express receiver are on the webhooks page.