Skip to content
yeetpostyeetpost

Posts

Post now, schedule, queue, draft, edit, retry a failed post, delete.


Publishing, scheduling and looking up posts.

Post plain text to one connection (simple variant)

POST/post/{connectionSlug}

The one-liner endpoint: the request body is the post itself, as plain text. It posts to a single connection and is the simplest thing to call from a shell script or a cron job.

This is the original v2 endpoint and it is not going away, but new integrations should prefer POST /posts, which takes JSON, posts to several connections at once, supports Idempotency-Key, and returns the post id.

Content-Type must be text/plain or application/x-www-form-urlencoded, or be omitted; anything else is rejected with 415.

Rate limit: 60 requests per minute per API key.

curl -X POST "https://api.yeetpost.com/api/v2/post/linkedin" \  -H "x-api-key: $YEETPOST_API_KEY" \  -H "content-type: text/plain" \  -d 'shipped the new api docs today'

Published to LinkedIn, X, Bluesky, Discord, Telegram or Mastodon

response 200
{  "platform": "linkedin",  "link": "https://www.linkedin.com/feed/update/urn:li:share:1234567890"}

Sent to SMS, email or Slack

response 200
{  "platform": "sms",  "sent": true}

Scheduled for later

response 200
{  "scheduled": true,  "scheduled_for": "2026-09-15T15:00:00.000Z"}

path parameters

connectionSlugstringrequired

Slug of the connection to post to, from GET /connections.

query parameters

scheduled_forstring <date-time>optional

ISO 8601 timestamp to send the post at. Must be in the future. Without it the post goes out immediately.

rewritebooleanoptional

AI polish for this post: true or false always wins over the connection's own mode and the account's default. Left out, the connection's mode decides. See the AI polish section. The one-liner has no JSON body to carry the field on, hence the query param.

The text to post. Must not be empty.

response 200

The post was published, or accepted for sending later.

headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset

response body, one of: Published / Sent / Scheduled

Published

platformstringrequired

linkedin | x | bluesky | discord | telegram | mastodon | reddit | facebook | instagram | threads | google_business

linkstringrequired

Permalink to the published post.

Sent

platformstringrequired

sms | email | slack

sentbooleanrequired

true

Scheduled

scheduledbooleanrequired

true

scheduled_forstring <date-time>required

errors

Empty body, bad scheduled_for, or unknown connection.

Missing or invalid API key.

Posting to X needs a payment method, would go past your own monthly X spend cap, or the account is at the free active-connection limit and not subscribed: posting to an idle connection, or to an older active one outside the newest 2, is refused.

You have used up your plan's posts or messages for this month, or a free account has hit the daily fair-use cap of 100 posts.

Unsupported Content-Type.

The platform refused the post, for example as a duplicate or a policy violation.

More than 60 requests in a minute for this API key. Back off and retry.

Something went wrong on our side. Quote the x-request-id when you report it. A failure can happen before the rate limit is checked, so this response carries the request id and nothing else.

The platform answered 429 or 5xx. Nothing was posted, so sending the same request again is safe.

No longer returned: a Discord post that gets no answer in time counts as sent, without a message id. Kept for older stored posts, where the message may have posted even though yeetpost never heard back.

What each code means and the fix: errors.

Post to one or more connections

POST/posts

Posts the same text to every connection in connectionSlugs, immediately, at scheduledFor, with queue: true into each connection's next free posting slot, or with isDraft: true into a draft that goes nowhere until you publish it. Duplicate slugs are ignored.

Partial failures are normal. As long as at least one slug matches a live connection, the response is 200 and every connection gets its own entry in results with a status of sent, scheduled or failed. Check the per-item status and error, not just the HTTP status. The request as a whole only fails (400 invalid_connection) when none of the slugs match a connection of yours.

Checking first. validateOnly: true runs every check and creates nothing: it answers 200 { valid, results } with each connection's text length and limit and its own error or none, every connection checked. No row, idempotency key, usage, queue slot or request log entry is made.

Idempotency. Send an Idempotency-Key header to make retries safe: the first request stores its response under the key and any later request with the same key and the same body replays that stored response verbatim, without posting again. Keys live for 24 hours and are scoped to your account.

Rate limit: 60 requests per minute per API key, shared with GET /posts.

curl -X POST "https://api.yeetpost.com/api/v2/posts" \  -H "x-api-key: $YEETPOST_API_KEY" \  -H "content-type: application/json" \  -H "Idempotency-Key: $(uuidgen)" \  -d '{  "text": "shipped the new api docs today",  "connectionSlugs": [    "linkedin",    "x"  ]}'

One connection sent, one refused by the platform

response 200
{  "results": [    {      "connectionSlug": "linkedin",      "status": "sent",      "postId": "6f1b0c8a-1f3d-4d3e-9a2b-2f5a1c9e7d10",      "url": "https://www.linkedin.com/feed/update/urn:li:share:1234567890",      "scheduledFor": null,      "threadCount": 0,      "hasFirstComment": false,      "error": null    },    {      "connectionSlug": "x",      "status": "failed",      "postId": "0b7e4a52-9f0c-4a1c-8b3a-6d2f5b1c0e44",      "url": null,      "scheduledFor": null,      "threadCount": 0,      "hasFirstComment": false,      "error": {        "code": "platform_rejected",        "message": "X: duplicate post was detected"      }    }  ]}

`queue: true` against a connection with no posting slots

response 200
{  "results": [    {      "connectionSlug": "linkedin",      "status": "failed",      "postId": null,      "url": null,      "scheduledFor": null,      "threadCount": 0,      "hasFirstComment": false,      "error": {        "code": "queue_empty",        "message": "That connection has no posting slots. Add some with PATCH /connections/{connectionId}/queue."      }    }  ]}

`queue: true` with every slot in the next 30 days taken

response 200
{  "results": [    {      "connectionSlug": "linkedin",      "status": "failed",      "postId": null,      "url": null,      "scheduledFor": null,      "threadCount": 0,      "hasFirstComment": false,      "error": {        "code": "queue_full",        "message": "Every posting slot within the next 30 days is taken. Cancel a queued post or add more slots."      }    }  ]}

An image for X on a connection made before image posting

response 200
{  "results": [    {      "connectionSlug": "x",      "status": "failed",      "postId": "0b7e4a52-9f0c-4a1c-8b3a-6d2f5b1c0e44",      "url": null,      "scheduledFor": null,      "threadCount": 0,      "hasFirstComment": false,      "error": {        "code": "reconnect_required",        "message": "Reconnect your X account to post images (a new permission is needed)."      }    }  ]}

Discord answered 429; nothing was posted, sending again is safe

response 200
{  "results": [    {      "connectionSlug": "discord",      "status": "failed",      "postId": "0b7e4a52-9f0c-4a1c-8b3a-6d2f5b1c0e44",      "url": null,      "scheduledFor": null,      "threadCount": 0,      "hasFirstComment": false,      "error": {        "code": "platform_unavailable",        "message": "Discord is rate limiting this account, so nothing went out. Send it again later."      }    }  ]}

Over the free connection limit and not subscribed, after the grace window

response 200
{  "results": [    {      "connectionSlug": "linkedin",      "status": "failed",      "postId": "0b7e4a52-9f0c-4a1c-8b3a-6d2f5b1c0e44",      "url": null,      "scheduledFor": null,      "threadCount": 0,      "hasFirstComment": false,      "error": {        "code": "connection_limit_exceeded",        "message": "This account already posted to 2 accounts in the last 30 days, the free limit. Subscribe to post to more, or wait until one has been idle for 30 days."      }    }  ]}

`validateOnly: true`, nothing created

response 200
{  "valid": false,  "results": [    {      "connectionSlug": "linkedin",      "valid": true,      "textLength": 32,      "textLimit": 3000,      "isTextLimitAdvisory": false,      "error": null    },    {      "connectionSlug": "bluesky",      "valid": false,      "textLength": 332,      "textLimit": 300,      "isTextLimitAdvisory": false,      "error": {        "code": "text_too_long",        "message": "Bluesky allows 300 characters; the text is 332.",        "field": "text"      }    }  ]}

A thread segment over the platform's limit

response 200
{  "results": [    {      "connectionSlug": "x",      "status": "failed",      "postId": null,      "url": null,      "scheduledFor": null,      "threadCount": 0,      "hasFirstComment": false,      "error": {        "code": "text_too_long",        "message": "X allows 280 characters; the text is 281.",        "field": "thread[1]"      }    }  ]}

header parameters

Idempotency-Keystring, 1 to 255 charactersoptional

A key of your choosing, 1 to 255 characters, that makes this request safe to retry. A uuid per logical post works well. Reusing a key with a different body is a 409.

request body (application/json)

textstring, at least 1 characterrequired

The text to post. Leading and trailing whitespace is trimmed.

connectionSlugsstring[], at least 1 itemrequired

Slugs to post to, from GET /connections. Duplicates are ignored.

scheduledForstring <date-time>optional

ISO 8601 timestamp to send at. Must be in the future. Leave it out to post immediately.

timezonestringoptional

IANA zone to read scheduledFor in when it carries no UTC offset, for example Europe/Helsinki. Refused with invalid_request when scheduledFor already has an offset, or when there is no scheduledFor to read.

queuebooleanoptional

Take each connection's next free posting slot instead of naming a time. The per-connection result comes back as scheduled with the slot it took in scheduledFor. Cannot be combined with scheduledFor. A connection with no slots fails that one item with queue_empty, and one whose slots are all taken inside the next 30 days fails it with queue_full.

mediaIdsstring <uuid>[], at most 5 itemsoptional

Ids of images to attach, from POST /media. Shorthand for media when no alt text is needed. Send only one of this, media and mediaUrls.

mediaobject[], at most 5 itemsoptional

Images to attach, each with optional alt text. Send only one of this, mediaIds and mediaUrls.

media[].idstring <uuid>required

Media id from POST /media.

media[].altTextstring, at most 1000 charactersoptional

Description of the image for screen readers. Overrides the alt text given at upload. Passed to X, Bluesky, Mastodon and LinkedIn. Telegram has no field for it, so it is dropped there.

mediaUrlsstring <uri>[], at most 5 itemsoptional

Images to attach, fetched by yeetpost instead of uploaded first. Each must be an https url; one that is not, that does not resolve to a public address, or that fails to fetch refuses the whole request with invalid_media_url, naming the index. Send only one of this, mediaIds and media.

threadstring[], at most 24 itemsoptional

Reply chain posted under the head post, one entry per reply, in order. X, Bluesky, Telegram and Mastodon only: any other connection, Discord included, refuses the item with shape_unsupported. Images stay on the head post.

firstCommentstring, at least 1 characteroptional

Comment posted under the post as its author. LinkedIn and Facebook only: any other connection refuses the item with shape_unsupported.

isDraftbooleanoptional

Store the post against every connection in connectionSlugs and publish nothing. Each item comes back as draft with a postId. A draft is validated against its connection the way a real send is (the connection is yours, the platform takes the shape and the images) but reserves nothing: no usage, no X fee, no queue slot and no webhook. Publish it with POST /posts/{postId}/publish. Cannot be combined with scheduledFor or queue.

tagsstring[], at most 10 itemsoptional

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

profileIdstring <uuid>optional

The end customer this post is going out for. Every slug in connectionSlugs has to belong to that profile, and one that does not fails that item with invalid_connection. Leave it out and every slug has to be one of your own connections, so a post can never reach a profile by accident.

rewritebooleanoptional

AI polish for this post: true or false always wins over the connection's own mode and the account's default. Leave it out and the connection's mode decides. No effect on a thread, which AI polish never touches. See the AI polish section.

textByPlatformobjectoptional

A platform's own text, keyed by platform id (linkedin, x, bluesky, and so on), used in place of the shared text for every connection in connectionSlugs on that platform. A platform left out gets the shared text.

validateOnlybooleanoptional

Run every check the real call runs (the body, the connections, the profile, the shape, the text, the images and scheduledFor) and answer without creating anything: no row, no idempotency reservation, no usage, no fair-use slot, no X fee, no queue slot, no webhook. It answers 200 with { valid, results }: one item per connection, each with its own error or null, and every connection is checked. A request refused as a whole (a bad body, a past scheduledFor, none of the slugs matching a connection of yours) answers the same 400 the real call would give. The test-key rule, queue: true against a connection with no free slot, the Telegram caption limit with an image, Reddit's title and SMS's 70 characters for non-GSM text are all checked. Usage limits, the fair-use cap and the connection cap are not. The call leaves nothing behind, not even a row in the request log. mediaUrls are not fetched.

response 200

One result per connection. Some of them may have failed.

headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset

response body, one of: variant 1 / variant 2

variant 1

resultsobject[]required

One entry per connection, in the order the slugs were given.

results[].connectionSlugstringrequired
results[].statusstringrequired

sent | scheduled | draft | failed

results[].postIdstring <uuid> | nullrequired

Id of the post row. null when nothing was stored, for example when the slug did not match a connection.

results[].urlstring | nullrequired

Permalink, for platforms that return one. null for SMS, email, Slack, for scheduled posts, and for a Telegram basic group, which has no message links at all.

results[].scheduledForstring <date-time> | nullrequired

When this post is due, for scheduled items.

results[].threadCountintegerrequired

How many replies actually went out under the head post. 0 when no thread was asked for. A thread X, Telegram or Mastodon refuses partway is deleted again, head included, and the item comes back failed; if a delete is refused too, or the platform is Bluesky, the item is sent at the count that is still up.

results[].hasFirstCommentbooleanrequired

Whether the first comment landed. False when none was asked for, and false when the platform refused it, which does not fail the post.

results[].warningstringoptional

Present only when AI polish was asked for and failed: the post went out under its original text, e.g. "AI rewrite failed, posted your original text: Your AI key was rejected". Absent on every other result.

results[].errorobject | nullrequired

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

results[].error.codestringrequired

Same vocabulary as the top-level error field.

results[].error.messagestringrequired
results[].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>.

variant 2

validbooleanrequired

True when every connection would be accepted.

resultsobject[]required

One entry per connection, in the order the slugs were given. Every connection is checked.

results[].connectionSlugstringrequired
results[].validbooleanrequired
results[].textLengthinteger | nullrequired

How long the text for this connection is, counted the way the platform counts it (graphemes on Bluesky, a weighted count on X, code points on Discord and Threads, Mastodon's own count with links as 23, characters elsewhere). Null when the slug did not resolve.

results[].textLimitinteger | nullrequired

The connection's text limit, null when it has none or the slug did not resolve.

results[].isTextLimitAdvisorybooleanrequired

True when textLimit is a target and not a ceiling (X): the item can be valid with a textLength over it.

results[].errorobject | nullrequired

The refusal the real call would give for this connection (invalid_connection, queue_empty, queue_full, media_unsupported, shape_unsupported, text_too_long), null when it would be accepted.

results[].error.codestringrequired
results[].error.messagestringrequired
results[].error.fieldstringoptional

Only on text_too_long: the part of the request that is too long.

A POST /posts with validateOnly: true: what the real call would answer for each connection, with nothing created. It answers 200 whether or not a connection would be refused, with the refusal in that connection's item, the way the real call does.

errors

Malformed body, bad scheduledFor, or none of the slugs match a connection of yours. A body that is not valid JSON is rejected before the rate limit is checked, so that one response carries x-request-id and no rate limit headers.

Missing or invalid API key.

This Idempotency-Key was already used with a different body, or its first request is still running.

More than 60 requests in a minute for this API key. Back off and retry.

Something went wrong on our side. Quote the x-request-id when you report it. A failure can happen before the rate limit is checked, so this response carries the request id and nothing else.

What each code means and the fix: errors.

List your posts

GET/posts

Your posts, newest first, one row per connection they went to.

Rate limit: 60 requests per minute per API key, shared with POST /posts.

curl "https://api.yeetpost.com/api/v2/posts" \  -H "x-api-key: $YEETPOST_API_KEY"
response 200
{  "posts": [    {      "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    }  ],  "limit": 25,  "offset": 0,  "total": 1,  "hasMore": false,  "nextOffset": null}

query parameters

statusstringoptional

Only return posts in this status.

sent | scheduled | processing | draft | failed | cancelled | deleted

tagstring[]optional

Only return posts carrying this tag. Repeat the parameter to narrow further: a post has to carry every tag you name, not any of them.

profileIdstring <uuid>optional

Only return the posts that went out through this profile's connections.

connectionSlugstring, at least 1 characteroptional

Only return posts sent to this connection's slug.

platformstringoptional

Only return posts sent to a connection on this platform.

sendGroupIdstring <uuid>optional

Only return the posts created by one POST /posts request.

scheduledAfterstring <date-time>optional

Only return posts scheduled at or after this ISO 8601 timestamp. Answers nothing for a post that was never scheduled.

scheduledBeforestring <date-time>optional

Only return posts scheduled at or before this ISO 8601 timestamp. Answers nothing for a post that was never scheduled.

createdAfterstring <date-time>optional

Only return posts created at or after this ISO 8601 timestamp.

createdBeforestring <date-time>optional

Only return posts created at or before this ISO 8601 timestamp.

sortstring, default created_descoptional

The order of the posts. Posts that were never scheduled come last on the scheduled orders.

created_desc | created_asc | scheduled_desc | scheduled_asc

limitinteger, 1 to 100, default 25optional

How many posts to return.

offsetinteger, min 0, default 0optional

How many posts to skip, for paging.

response 200

A page of posts, plus the limit and offset that produced it, the total that match, and where the next page starts.

headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset

response body

postsobject[]required
posts[].idstring <uuid>required
posts[].connectionSlugstringrequired
posts[].textstringrequired

What actually went out. Equal to originalText unless AI polish rewrote this post.

posts[].originalTextstringrequired

What you sent, before AI polish, if any, touched it.

posts[].aiRewriteStatusstring | nullrequired

null: AI polish never touched this post. "rewritten": text is the model's own words. "failed": AI polish tried and failed, and text is the original after all. "skipped": the post's shape does not support a rewrite (a thread).

rewritten | failed | skipped | null

posts[].statusstringrequired

scheduled is waiting to be sent, processing is being sent right now, draft is stored and has gone nowhere, cancelled was a scheduled post you cancelled, deleted was sent and then taken back down on its platform.

sent | scheduled | processing | draft | failed | cancelled | deleted

posts[].urlstring | nullrequired

Permalink, for platforms that return one. null for a Telegram basic group, which has no message links at all.

posts[].scheduledForstring <date-time> | nullrequired

Same format as Timestamp, or null.

posts[].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.

posts[].platformPostIdstring | nullrequired

The platform's own id for the head post (tweet id, at-uri, status id, message id, LinkedIn urn). This is what a delete works from. Null on a post that never reached a platform, and on one sent before yeetpost recorded these.

posts[].deletedAtstring <date-time> | nullrequired

When the post was deleted on its platform. Null while it is still up.

posts[].sendGroupIdstring <uuid> | nullrequired

Every post created by one request shares this id, so you can group the targets of one send the way the yeetpost console does. Null on a post sent before this shipped.

posts[].errorobject | nullrequired

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

posts[].error.codestringrequired

Same vocabulary as the top-level error field.

posts[].error.messagestringrequired
posts[].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>.

posts[].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=.

posts[].profileIdstring <uuid> | nullrequired

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

posts[].mediaobject[]optional

Only on GET /posts and GET /posts/{postId}. The images attached to the post, in posting order.

posts[].media[].idstring <uuid>required
posts[].media[].altTextstring | nullrequired
posts[].threadstring[]optional

Only on GET /posts and GET /posts/{postId}. The reply chain under the head post, as it was given. Empty for a single post, which is every post made before shapes shipped.

posts[].firstCommentstring | nulloptional

Only on GET /posts and GET /posts/{postId}. The author's own comment under the share, null when there is none.

posts[].publishedAtstring <date-time> | nulloptional

Only on GET /posts and GET /posts/{postId}. When the post actually went out, null while it is still a draft, still pending, or never sent at all.

posts[].textTruncatedbooleanrequired

Always false on this endpoint, which answers with the whole text.

limitintegerrequired
offsetintegerrequired
totalintegerrequired

How many posts match the filters in all, whatever page this is.

hasMorebooleanrequired
nextOffsetinteger | nullrequired

The offset of the next page, null on the last one.

errors

Invalid query parameters.

Missing or invalid API key.

More than 60 requests in a minute for this API key. Back off and retry.

Something went wrong on our side. Quote the x-request-id when you report it. A failure can happen before the rate limit is checked, so this response carries the request id and nothing else.

What each code means and the fix: errors.

Get a single post

GET/posts/{postId}

Returns one post of yours. Posts belonging to someone else are 404, same as posts that do not exist.

Rate limit: 60 requests per minute per API key, shared with DELETE /posts/{postId}.

curl "https://api.yeetpost.com/api/v2/posts/6f1b0c8a-1f3d-4d3e-9a2b-2f5a1c9e7d10" \  -H "x-api-key: $YEETPOST_API_KEY"
response 200
{  "post": {    "id": "6f1b0c8a-1f3d-4d3e-9a2b-2f5a1c9e7d10",    "connectionSlug": "linkedin",    "text": "shipped the new api docs today",    "status": "scheduled",    "url": null,    "scheduledFor": "2026-09-15T15:00:00.000Z",    "createdAt": "2026-08-26T09:24:11.412Z",    "error": null  }}

path parameters

postIdstring <uuid>required

Id of the post, as returned by POST /posts or GET /posts.

response 200

The post.

headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset

response body

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

What actually went out. Equal to originalText unless AI polish rewrote this post.

post.originalTextstringrequired

What you sent, before AI polish, if any, touched it.

post.aiRewriteStatusstring | nullrequired

null: AI polish never touched this post. "rewritten": text is the model's own words. "failed": AI polish tried and failed, and text is the original after all. "skipped": the post's shape does not support a rewrite (a thread).

rewritten | failed | skipped | null

post.statusstringrequired

scheduled is waiting to be sent, processing is being sent right now, draft is stored and has gone nowhere, cancelled was a scheduled post you cancelled, deleted was sent and then taken back down on its platform.

sent | scheduled | processing | draft | failed | cancelled | deleted

post.urlstring | nullrequired

Permalink, for platforms that return one. null for a Telegram basic group, which has no message links at all.

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.platformPostIdstring | nullrequired

The platform's own id for the head post (tweet id, at-uri, status id, message id, LinkedIn urn). This is what a delete works from. Null on a post that never reached a platform, and on one sent before yeetpost recorded these.

post.deletedAtstring <date-time> | nullrequired

When the post was deleted on its platform. Null while it is still up.

post.sendGroupIdstring <uuid> | nullrequired

Every post created by one request shares this id, so you can group the targets of one send the way the yeetpost console does. Null on a post sent before this shipped.

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.

post.mediaobject[]optional

Only on GET /posts and GET /posts/{postId}. The images attached to the post, in posting order.

post.media[].idstring <uuid>required
post.media[].altTextstring | nullrequired
post.threadstring[]optional

Only on GET /posts and GET /posts/{postId}. The reply chain under the head post, as it was given. Empty for a single post, which is every post made before shapes shipped.

post.firstCommentstring | nulloptional

Only on GET /posts and GET /posts/{postId}. The author's own comment under the share, null when there is none.

post.publishedAtstring <date-time> | nulloptional

Only on GET /posts and GET /posts/{postId}. When the post actually went out, null while it is still a draft, still pending, or never sent at all.

errors

The post id is not a valid id.

Missing or invalid API key.

No post of yours with that id.

More than 60 requests in a minute for this API key. Back off and retry.

Something went wrong on our side. Quote the x-request-id when you report it. A failure can happen before the rate limit is checked, so this response carries the request id and nothing else.

What each code means and the fix: errors.

Cancel a scheduled post, delete a draft, or delete a published one

DELETE/posts/{postId}

One verb, three meanings, decided by what the post is.

A post that is still waiting to be sent is cancelled: the row is kept and moves to status cancelled, and nothing was published in the first place. If the worker claims the post while your request is in flight you get 409 post_not_cancellable.

A draft is deleted: a draft never went out, so there is no record to keep. The answer is { id, deleted: true } and the row and its images are gone. A post whose send failed before anything reached the platform is deleted the same way, like a draft; a send that may have reached the platform (a timeout or an interrupted send) is treated as sent.

A post that already went out is deleted on its platform: the head post and every reply this send published under it come down, newest first, and the row moves to status deleted with a deletedAt. X, Bluesky, Mastodon, Telegram, Discord and LinkedIn have a delete; SMS, email and Slack do not and answer 400 delete_unsupported. A platform that refuses the delete answers 422 platform_rejected and the post stays up. Deleting the same post twice answers 200 with the same body: it is already gone.

Nothing is refunded. X charges per tweet when the post goes out and keeps that fee for a tweet you delete later.

Rate limit: 60 requests per minute per API key, shared with GET /posts/{postId}.

curl -X DELETE "https://api.yeetpost.com/api/v2/posts/6f1b0c8a-1f3d-4d3e-9a2b-2f5a1c9e7d10" \  -H "x-api-key: $YEETPOST_API_KEY"

A scheduled post that had not been sent yet

response 200
{  "id": "6f1b0c8a-1f3d-4d3e-9a2b-2f5a1c9e7d10",  "status": "cancelled"}

A draft, deleted for good

response 200
{  "id": "6f1b0c8a-1f3d-4d3e-9a2b-2f5a1c9e7d10",  "deleted": true}

A published post, taken down on its platform

response 200
{  "id": "6f1b0c8a-1f3d-4d3e-9a2b-2f5a1c9e7d10",  "status": "deleted",  "deletedAt": "2026-08-29T11:02:44.108Z"}

path parameters

postIdstring <uuid>required

Id of the post, as returned by POST /posts or GET /posts.

response 200

The post is cancelled, the draft is deleted, or the post is deleted on its platform.

headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset

response body, one of: Cancelled scheduled post / Deleted draft / Deleted published post

Cancelled scheduled post

idstring <uuid>required
statusstringrequired

cancelled

Deleted draft

idstring <uuid>required
deletedbooleanrequired

true

Deleted published post

idstring <uuid>required
statusstringrequired

deleted

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.

errors

Malformed post id, a post that was never published, or a platform with no delete at all.

Missing or invalid API key.

No post of yours with that id.

The worker claimed the post while the request was in flight. The post was not cancelled.

The platform refused to delete the post, so it is still up. delete_error is remembered on the post.

More than 60 requests in a minute for this API key. Back off and retry.

Something went wrong on our side. Quote the x-request-id when you report it. A failure can happen before the rate limit is checked, so this response carries the request id and nothing else.

What each code means and the fix: errors.

Edit a draft or a scheduled post

PATCH/posts/{postId}

Edits a draft, or a pending scheduled post, in place: its text, its shape, its images, its tags and, for a scheduled post, its time. Only what you send is written.

Only a draft or a post still pending can be edited. A post the worker has already claimed is 409 post_not_editable. A post that is sent, cancelled or failed is a record of what happened, so editing one is 400 invalid_request.

scheduledFor, timezone and queue move a pending scheduled post's own time; a draft refuses all three with 400 invalid_request, since it has nowhere to be sent at yet. Naming a new time, or queue: true on a post not already queued, fires post.rescheduled with reason edited once the new time is committed. A text edit is stored as final: it is your own words from here on, so any AI polish the post already carried is cleared rather than run again.

Whatever the post ends up holding is validated against its connection again, so an edit cannot leave it in a shape the connection could never publish.

Rate limit: 60 requests per minute per API key, shared with GET /posts/{postId}.

curl -X PATCH "https://api.yeetpost.com/api/v2/posts/6f1b0c8a-1f3d-4d3e-9a2b-2f5a1c9e7d10" \  -H "x-api-key: $YEETPOST_API_KEY" \  -H "content-type: application/json" \  -d '{  "text": "shipped the new api docs today, with drafts",  "tags": [    "launch",    "q3"  ]}'
response 200
{  "post": {    "id": "6f1b0c8a-1f3d-4d3e-9a2b-2f5a1c9e7d10",    "connectionSlug": "linkedin",    "text": "shipped the new api docs today, with drafts",    "status": "draft",    "url": null,    "scheduledFor": null,    "createdAt": "2026-08-26T09:24:11.412Z",    "tags": [      "launch",      "q3"    ],    "error": null  }}

path parameters

postIdstring <uuid>required

Id of the post, as returned by POST /posts or GET /posts.

request body (application/json)

textstring, at least 1 characteroptional

The new text. Leading and trailing whitespace is trimmed. On a scheduled post this is stored as final: any AI polish it already carried is cleared rather than run again.

threadstring[], at most 24 itemsoptional

The new reply chain under the head post. An empty array, or a firstComment on its own, clears it.

firstCommentstring | null, at least 1 characteroptional

The new comment under the share, or null to clear it.

mediaIdsstring <uuid>[], at most 5 itemsoptional

The images the post should carry from now on. The list replaces what it had; an id you leave out is taken off it. Send either this or media, never both.

mediaobject[], at most 5 itemsoptional

The same replacement, with alt text per image. Send either this or mediaIds, never both.

media[].idstring <uuid>required

Media id from POST /media.

media[].altTextstring, at most 1000 charactersoptional

Description of the image for screen readers. Overrides the alt text given at upload. Passed to X, Bluesky, Mastodon and LinkedIn. Telegram has no field for it, so it is dropped there.

tagsstring[], at most 10 itemsoptional

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

scheduledForstring <date-time>optional

A pending scheduled post's new send time. Must be in the future. Refused on a draft. Cannot be combined with queue.

timezonestringoptional

IANA zone to read scheduledFor in when it carries no UTC offset, for example Europe/Helsinki.

queuebooleanoptional

Take the connection's next free posting slot instead of naming a time. Already queued, this is a no-op: the slot it holds is left alone. Refused on a draft. Cannot be combined with scheduledFor.

response 200

The draft as it now stands.

headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset

response body

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

What actually went out. Equal to originalText unless AI polish rewrote this post.

post.originalTextstringrequired

What you sent, before AI polish, if any, touched it.

post.aiRewriteStatusstring | nullrequired

null: AI polish never touched this post. "rewritten": text is the model's own words. "failed": AI polish tried and failed, and text is the original after all. "skipped": the post's shape does not support a rewrite (a thread).

rewritten | failed | skipped | null

post.statusstringrequired

scheduled is waiting to be sent, processing is being sent right now, draft is stored and has gone nowhere, cancelled was a scheduled post you cancelled, deleted was sent and then taken back down on its platform.

sent | scheduled | processing | draft | failed | cancelled | deleted

post.urlstring | nullrequired

Permalink, for platforms that return one. null for a Telegram basic group, which has no message links at all.

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.platformPostIdstring | nullrequired

The platform's own id for the head post (tweet id, at-uri, status id, message id, LinkedIn urn). This is what a delete works from. Null on a post that never reached a platform, and on one sent before yeetpost recorded these.

post.deletedAtstring <date-time> | nullrequired

When the post was deleted on its platform. Null while it is still up.

post.sendGroupIdstring <uuid> | nullrequired

Every post created by one request shares this id, so you can group the targets of one send the way the yeetpost console does. Null on a post sent before this shipped.

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.

post.mediaobject[]optional

Only on GET /posts and GET /posts/{postId}. The images attached to the post, in posting order.

post.media[].idstring <uuid>required
post.media[].altTextstring | nullrequired
post.threadstring[]optional

Only on GET /posts and GET /posts/{postId}. The reply chain under the head post, as it was given. Empty for a single post, which is every post made before shapes shipped.

post.firstCommentstring | nulloptional

Only on GET /posts and GET /posts/{postId}. The author's own comment under the share, null when there is none.

post.publishedAtstring <date-time> | nulloptional

Only on GET /posts and GET /posts/{postId}. When the post actually went out, null while it is still a draft, still pending, or never sent at all.

errors

Malformed post id or body, a shape or image the connection cannot take, a time that is not in the future, timing sent for a draft, or a post that is neither a draft nor a pending scheduled post.

Missing or invalid API key.

queue: true, and every posting slot within the queue horizon is already taken.

No post of yours with that id.

The worker claimed the post while the request was in flight. The post was not edited.

More than 60 requests in a minute for this API key. Back off and retry.

Something went wrong on our side. Quote the x-request-id when you report it. A failure can happen before the rate limit is checked, so this response carries the request id and nothing else.

What each code means and the fix: errors.

Publish a draft

POST/posts/{postId}/publish

Turns a draft into a real send: now, at scheduledFor, or with queue: true into the connection's next free posting slot. The stored text, shape, images and tags go out, and the post keeps the id it had as a draft, so an id you are already holding stays valid.

From here on it is an ordinary post: it is billed, counted against your limits and fair-use cap, and it fires webhooks. The draft keeps the mode it was created with, so one stored by a test key publishes as a test post whichever key publishes it, and it goes to the connection it was created against by id: delete that connection and the publish is 400 invalid_connection, even if a new connection has taken its slug.

A refusal you can still settle leaves the draft exactly where it was, so you can fix the problem and publish the same id again: queue_empty, queue_full, payment_method_required, spend_cap_reached, fair_use_reached and connection_limit_exceeded. Every other refusal is what happened to the post, so the draft becomes a failed post.

A post id that is not a draft any more is 404 not_found, which is what a second publish of one draft gets. Two publishes of one draft at the same moment settle the same way, one 200 and one 404, and the post goes out once.

Takes an Idempotency-Key the same way POST /posts does.

Rate limit: 60 requests per minute per API key.

curl -X POST "https://api.yeetpost.com/api/v2/posts/6f1b0c8a-1f3d-4d3e-9a2b-2f5a1c9e7d10/publish" \  -H "x-api-key: $YEETPOST_API_KEY" \  -H "content-type: application/json" \  -H "Idempotency-Key: $(uuidgen)" \  -d '{  "scheduledFor": "2026-09-15T15:00:00Z"}'
response 200
{  "result": {    "connectionSlug": "linkedin",    "status": "sent",    "postId": "6f1b0c8a-1f3d-4d3e-9a2b-2f5a1c9e7d10",    "url": "https://www.linkedin.com/feed/update/urn:li:share:1234567890",    "scheduledFor": null,    "threadCount": 0,    "hasFirstComment": false,    "error": null  }}

path parameters

postIdstring <uuid>required

Id of the draft, as returned by POST /posts with isDraft: true.

header parameters

Idempotency-Keystring, 1 to 255 charactersoptional

A key of your choosing, 1 to 255 characters, that makes this request safe to retry. A uuid per logical post works well. Reusing a key with a different body is a 409.

request body (application/json)

scheduledForstring <date-time>optional

ISO 8601 timestamp to send at, instead of now. Must be in the future.

timezonestringoptional

IANA zone to read scheduledFor in when it carries no UTC offset, for example Europe/Helsinki.

queuebooleanoptional

Take the connection's next free posting slot instead of naming a time. Cannot be combined with scheduledFor. A queue with no free slot answers queue_empty or queue_full in result.error and leaves the draft where it was.

response 200

The result of the send. It may have failed: read result.status and result.error.

headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset

response body

resultobjectrequired
result.connectionSlugstringrequired
result.statusstringrequired

sent | scheduled | draft | failed

result.postIdstring <uuid> | nullrequired

Id of the post row. null when nothing was stored, for example when the slug did not match a connection.

result.urlstring | nullrequired

Permalink, for platforms that return one. null for SMS, email, Slack, for scheduled posts, and for a Telegram basic group, which has no message links at all.

result.scheduledForstring <date-time> | nullrequired

When this post is due, for scheduled items.

result.threadCountintegerrequired

How many replies actually went out under the head post. 0 when no thread was asked for. A thread X, Telegram or Mastodon refuses partway is deleted again, head included, and the item comes back failed; if a delete is refused too, or the platform is Bluesky, the item is sent at the count that is still up.

result.hasFirstCommentbooleanrequired

Whether the first comment landed. False when none was asked for, and false when the platform refused it, which does not fail the post.

result.warningstringoptional

Present only when AI polish was asked for and failed: the post went out under its original text, e.g. "AI rewrite failed, posted your original text: Your AI key was rejected". Absent on every other result.

result.errorobject | nullrequired

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

result.error.codestringrequired

Same vocabulary as the top-level error field.

result.error.messagestringrequired
result.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>.

errors

Malformed post id, or a body that names both a time and a slot.

Missing or invalid API key.

No draft of yours has this id. A draft that was already published is gone as a draft.

This Idempotency-Key was already used with a different body, or its first request is still running.

More than 60 requests in a minute for this API key. Back off and retry.

Something went wrong on our side. Quote the x-request-id when you report it. A failure can happen before the rate limit is checked, so this response carries the request id and nothing else.

What each code means and the fix: errors.

Retry a failed post

POST/posts/{postId}/retry

Sends a failed post again. Only a post that actually failed can be retried; everything else is 400 invalid_request.

The text, images and shape sent are exactly what was stored on the first attempt, the same way the console's own retry works. A retry already in flight is 409 already_retrying rather than a second send.

Everything that can refuse an immediate send can refuse a retry the same way: a monthly limit is 403 limit_exceeded, the fair-use cap is 403 fair_use_reached, a missing payment method or X fee is 402, the platform rejecting it is 422 platform_rejected, and the platform being unreachable is 503 platform_unavailable. A Discord message POST that gets no answer in time is not a failure: Discord creates the message almost at once but can take half a minute to answer, so the post counts as sent, without a message id, and cannot be deleted from yeetpost. A connection that was disconnected since the post first went out is 400 invalid_connection.

Fires post.sent or post.failed the same way the original send did.

Rate limit: 60 requests per minute per API key.

curl -X POST "https://api.yeetpost.com/api/v2/posts/6f1b0c8a-1f3d-4d3e-9a2b-2f5a1c9e7d10/retry" \  -H "x-api-key: $YEETPOST_API_KEY"
response 200
{  "id": "6f1b0c8a-1f3d-4d3e-9a2b-2f5a1c9e7d10",  "status": "sent"}

path parameters

postIdstring <uuid>required

Id of the post, as returned by POST /posts or GET /posts.

response 200

Sent.

headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset

response body

idstring <uuid>required
statusstringrequired

sent

warningstringoptional

Set only on a retry sent inside the connection-limit grace window.

errors

Malformed post id, or the post did not fail and so cannot be retried.

Missing or invalid API key.

Posting to X needs a payment method, would go past your own monthly X spend cap, or the account is at the free active-connection limit and not subscribed: posting to an idle connection, or to an older active one outside the newest 2, is refused.

You have used up your plan's posts or messages for this month, or a free account has hit the daily fair-use cap of 100 posts.

No post of yours with that id.

This post is already being sent again.

The platform refused the post, for example as a duplicate or a policy violation.

More than 60 requests in a minute for this API key. Back off and retry.

Something went wrong on our side. Quote the x-request-id when you report it. A failure can happen before the rate limit is checked, so this response carries the request id and nothing else.

The platform answered 429 or 5xx. Nothing was posted, so sending the same request again is safe.

No longer returned: a Discord post that gets no answer in time counts as sent, without a message id. Kept for older stored posts, where the message may have posted even though yeetpost never heard back.

What each code means and the fix: errors.

Draw an image card for a post

POST/posts/{postId}/card

Writes an image card from the post's own text in its connection's look, renders it at 1200x1200 as a jpeg and returns it, so the user can see it before it is attached. One model call writes the card; the post text is treated as data, never as instructions. The connection needs a look with at least one example card, which the card follows for style only. Different posts give different cards.

With attach true the image is appended to the post's images; only a draft or a scheduled post takes one. Without it, any post can be previewed and nothing on the post changes. The card returns as mediaId: read the picture with GET /media/{mediaId}. html is the card's source, so a card the user likes can be saved into the look's example cards.

The card counts as one render against the monthly allowance (429 render_allowance_reached, checked before anything is drawn) and the per-account render pace (429 render_rate_limited). Refusals: 404 not_found, 400 look_missing, 400 images_not_supported, 400 post_not_editable, 400 too_many_media, 503 card_unavailable and 502 card_invalid (try again). When the card is made but the post refuses it, the error carries the mediaId of the kept image.

Rate limit: 20 requests per minute per API key.

curl -X POST "https://api.yeetpost.com/api/v2/posts/6f1b0c8a-1f3d-4d3e-9a2b-2f5a1c9e7d10/card" \  -H "x-api-key: $YEETPOST_API_KEY" \  -H "content-type: application/json"
response 200
{  "mediaId": "0d6f5b3e-8a41-4c53-b6c2-5e0d9f1a7b22",  "html": "<html>...</html>",  "attached": false,  "rendersUsed": 12,  "rendersLimit": 300}

path parameters

postIdstring <uuid>required

Id of the post, as returned by POST /posts or GET /posts.

request body (application/json)

instructionsstring, at most 2000 charactersoptional

What the user wants on the card, in their words, for example "use a chart".

attachbooleanoptional

Add the card to the post, after its images. Leave out to preview first.

response 200

The card, drawn.

headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset

response body

mediaIdstring <uuid>required

The card as an image. Read it with GET /media/{mediaId}.

htmlstringrequired

The card as HTML, to keep as an example card in the look. Put it first in exampleCards: cards follow the first example card.

attachedbooleanrequired

Whether the card was added to the post.

rendersUsedintegerrequired

Renders used this month, this card included.

rendersLimitintegerrequired

Renders allowed per month.

errors

Malformed request, the connection has no look with an example card, the platform takes no images, the post cannot take an image, or it already holds the most images.

Missing or invalid API key.

No post of yours with that id.

More than 60 requests in a minute for this API key. Back off and retry.

Something went wrong on our side. Quote the x-request-id when you report it. A failure can happen before the rate limit is checked, so this response carries the request id and nothing else.

The drawn card could not be used. Try again.

The card could not be drawn just now. Try again.

What each code means and the fix: errors.