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
{ "platform": "linkedin", "link": "https://www.linkedin.com/feed/update/urn:li:share:1234567890"}Sent to SMS, email or Slack
{ "platform": "sms", "sent": true}Scheduled for later
{ "scheduled": true, "scheduled_for": "2026-09-15T15:00:00.000Z"}path parameters
Slug of the connection to post to, from GET /connections.
query parameters
ISO 8601 timestamp to send the post at. Must be in the future. Without it the post goes out immediately.
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
linkedin | x | bluesky | discord | telegram | mastodon | reddit | facebook | instagram | threads | google_business
Permalink to the published post.
Sent
sms | email | slack
true
Scheduled
true
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
{ "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
{ "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
{ "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
{ "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
{ "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
{ "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
{ "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
{ "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
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)
The text to post. Leading and trailing whitespace is trimmed.
Slugs to post to, from GET /connections. Duplicates are ignored.
ISO 8601 timestamp to send at. Must be in the future. Leave it out to post immediately.
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.
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.
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.
Images to attach, each with optional alt text. Send only one of this, mediaIds and mediaUrls.
Media id from POST /media.
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.
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.
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.
Comment posted under the post as its author. LinkedIn and Facebook only: any other connection refuses the item with shape_unsupported.
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.
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 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.
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.
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.
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
One entry per connection, in the order the slugs were given.
sent | scheduled | draft | failed
Id of the post row. null when nothing was stored, for example when the slug did not match a connection.
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.
When this post is due, for scheduled items.
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.
Whether the first comment landed. False when none was asked for, and false when the platform refused it, which does not fail the post.
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.
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>.
variant 2
True when every connection would be accepted.
One entry per connection, in the order the slugs were given. Every connection is checked.
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.
The connection's text limit, null when it has none or the slug did not resolve.
True when textLimit is a target and not a ceiling (X): the item can be valid with a textLength over it.
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.
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"{ "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
Only return posts in this status.
sent | scheduled | processing | draft | failed | cancelled | deleted
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.
Only return the posts that went out through this profile's connections.
Only return posts sent to this connection's slug.
Only return posts sent to a connection on this platform.
Only return the posts created by one POST /posts request.
Only return posts scheduled at or after this ISO 8601 timestamp. Answers nothing for a post that was never scheduled.
Only return posts scheduled at or before this ISO 8601 timestamp. Answers nothing for a post that was never scheduled.
Only return posts created at or after this ISO 8601 timestamp.
Only return posts created at or before this ISO 8601 timestamp.
The order of the posts. Posts that were never scheduled come last on the scheduled orders.
created_desc | created_asc | scheduled_desc | scheduled_asc
How many posts to return.
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
What actually went out. Equal to originalText unless AI polish rewrote this post.
What you sent, before AI polish, if any, touched it.
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
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
Permalink, for platforms that return one. null for a Telegram basic group, which has no message links at all.
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.
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.
When the post was deleted on its platform. Null while it is still up.
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.
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.
Only on GET /posts and GET /posts/{postId}. The images attached to the post, in posting order.
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.
Only on GET /posts and GET /posts/{postId}. The author's own comment under the share, null when there is none.
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.
Always false on this endpoint, which answers with the whole text.
How many posts match the filters in all, whatever page this is.
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"{ "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
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
What actually went out. Equal to originalText unless AI polish rewrote this post.
What you sent, before AI polish, if any, touched it.
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
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
Permalink, for platforms that return one. null for a Telegram basic group, which has no message links at all.
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.
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.
When the post was deleted on its platform. Null while it is still up.
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.
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.
Only on GET /posts and GET /posts/{postId}. The images attached to the post, in posting order.
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.
Only on GET /posts and GET /posts/{postId}. The author's own comment under the share, null when there is none.
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
{ "id": "6f1b0c8a-1f3d-4d3e-9a2b-2f5a1c9e7d10", "status": "cancelled"}A draft, deleted for good
{ "id": "6f1b0c8a-1f3d-4d3e-9a2b-2f5a1c9e7d10", "deleted": true}A published post, taken down on its platform
{ "id": "6f1b0c8a-1f3d-4d3e-9a2b-2f5a1c9e7d10", "status": "deleted", "deletedAt": "2026-08-29T11:02:44.108Z"}path parameters
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
cancelled
Deleted draft
true
Deleted published post
deleted
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" ]}'{ "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
Id of the post, as returned by POST /posts or GET /posts.
request body (application/json)
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.
The new reply chain under the head post. An empty array, or a firstComment on its own, clears it.
The new comment under the share, or null to clear it.
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.
The same replacement, with alt text per image. Send either this or mediaIds, never both.
Media id from POST /media.
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.
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=.
A pending scheduled post's new send time. Must be in the future. Refused on a draft. Cannot be combined with queue.
IANA zone to read scheduledFor in when it carries no UTC offset, for example Europe/Helsinki.
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
What actually went out. Equal to originalText unless AI polish rewrote this post.
What you sent, before AI polish, if any, touched it.
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
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
Permalink, for platforms that return one. null for a Telegram basic group, which has no message links at all.
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.
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.
When the post was deleted on its platform. Null while it is still up.
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.
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.
Only on GET /posts and GET /posts/{postId}. The images attached to the post, in posting order.
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.
Only on GET /posts and GET /posts/{postId}. The author's own comment under the share, null when there is none.
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"}'{ "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
Id of the draft, as returned by POST /posts with isDraft: true.
header parameters
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)
ISO 8601 timestamp to send at, instead of now. Must be in the future.
IANA zone to read scheduledFor in when it carries no UTC offset, for example Europe/Helsinki.
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
sent | scheduled | draft | failed
Id of the post row. null when nothing was stored, for example when the slug did not match a connection.
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.
When this post is due, for scheduled items.
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.
Whether the first comment landed. False when none was asked for, and false when the platform refused it, which does not fail the post.
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.
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>.
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"{ "id": "6f1b0c8a-1f3d-4d3e-9a2b-2f5a1c9e7d10", "status": "sent"}path parameters
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
sent
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"{ "mediaId": "0d6f5b3e-8a41-4c53-b6c2-5e0d9f1a7b22", "html": "<html>...</html>", "attached": false, "rendersUsed": 12, "rendersLimit": 300}path parameters
Id of the post, as returned by POST /posts or GET /posts.
request body (application/json)
What the user wants on the card, in their words, for example "use a chart".
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
The card as an image. Read it with GET /media/{mediaId}.
The card as HTML, to keep as an example card in the look. Put it first in exampleCards: cards follow the first example card.
Whether the card was added to the post.
Renders used this month, this card included.
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.