API reference
Every endpoint on https://api.yeetpost.com/api/v2, generated from the OpenAPI spec (API version 2.0.0). Nothing here is hand-maintained.
base url
https://api.yeetpost.com/api/v2
version
2.0.0
auth
x-api-key
x-api-key or as Authorization: Bearer. The MCP endpoint also takes an OAuth 2.1 access token, for clients that have a browser and nowhere to paste a key. That flow and its discovery documents sit outside https://api.yeetpost.com/api/v2, so they are not listed below; the MCP page walks through it.connections
The accounts and channels you can post to.
List your connections
Lists every connection of yours that has not been deleted, with the slug you use to post to it and the timezone and posting slots its queue runs on.
Rate limit: 60 requests per minute per API key.
curl "https://api.yeetpost.com/api/v2/connections" \
-H "x-api-key: $YEETPOST_API_KEY"query parameters
Only return the connections of this profile. Left out, every connection of yours is listed and each item says which profile it belongs to.
response 200
Your connections.
headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset
response body
The connection's id, which the queue endpoints take in their path.
The identifier you post to, for example linkedin or alexdoe_x.
Which platform is behind the slug. Branch on this rather than on the wording of description: sms, email and slack are messaging channels, the rest are places you broadcast to.
linkedin | x | bluesky | discord | telegram | mastodon | sms | email | slack | sandbox
Human-readable label for the account or channel behind the slug.
IANA zone the posting slots are read in, for example Europe/Helsinki. UTC until you set one.
The connection's posting slots, in the order they were set. Empty when the connection has no queue.
Day of the week the slot falls on. 0 is Sunday.
24 hour HH:MM wall time, read on the connection's own clock.
The profile the connection belongs to. Null when it is one of your own accounts rather than an end customer's.
{
"connections": [
{
"id": "3f2a1b4c-5d6e-4f70-8a9b-0c1d2e3f4a5b",
"slug": "linkedin",
"platformId": "linkedin",
"description": "Alex Doe (LinkedIn)",
"timezone": "Europe/Helsinki",
"queueSlots": [
{
"weekday": 1,
"time": "09:00"
},
{
"weekday": 4,
"time": "17:30"
}
]
},
{
"id": "9c8b7a65-4d3e-4f21-8b0a-7c6d5e4f3a2b",
"slug": "x",
"platformId": "x",
"description": "@alexdoe (X)",
"timezone": "UTC",
"queueSlots": []
}
]
}errors
A profileId that is not a valid profile id, or that is not one of your profiles.
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.
Every code is on the errors page.
Get a single connection
Returns one connection of yours, with the timezone and posting slots its queue runs on.
Rate limit: 60 requests per minute per API key.
curl "https://api.yeetpost.com/api/v2/connections/3f2a1b4c-5d6e-4f70-8a9b-0c1d2e3f4a5b" \
-H "x-api-key: $YEETPOST_API_KEY"path parameters
Id of the connection, as returned by GET /connections.
response 200
The connection.
headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset
response body
The connection's id, which the queue endpoints take in their path.
The identifier you post to, for example linkedin or alexdoe_x.
Which platform is behind the slug. Branch on this rather than on the wording of description: sms, email and slack are messaging channels, the rest are places you broadcast to.
linkedin | x | bluesky | discord | telegram | mastodon | sms | email | slack | sandbox
Human-readable label for the account or channel behind the slug.
IANA zone the posting slots are read in, for example Europe/Helsinki. UTC until you set one.
The connection's posting slots, in the order they were set. Empty when the connection has no queue.
Day of the week the slot falls on. 0 is Sunday.
24 hour HH:MM wall time, read on the connection's own clock.
The profile the connection belongs to. Null when it is one of your own accounts rather than an end customer's.
{
"connection": {
"id": "3f2a1b4c-5d6e-4f70-8a9b-0c1d2e3f4a5b",
"slug": "linkedin",
"platformId": "linkedin",
"description": "Alex Doe (LinkedIn)",
"timezone": "Europe/Helsinki",
"queueSlots": [
{
"weekday": 1,
"time": "09:00"
},
{
"weekday": 4,
"time": "17:30"
}
]
}
}errors
Malformed connection id.
Missing or invalid API key.
No connection of yours with that id. Somebody else's connection is a 404 too.
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.
Every code is on the errors page.
List what is waiting in a connection's queue
The posts this connection has queued and not sent yet, earliest slot first. A post leaves the queue when it goes out, fails, or is cancelled with DELETE /posts/{postId}, and cancelling hands its slot back to the next post queued.
Rate limit: 60 requests per minute per API key, shared with PATCH /connections/{connectionId}/queue.
curl "https://api.yeetpost.com/api/v2/connections/3f2a1b4c-5d6e-4f70-8a9b-0c1d2e3f4a5b/queue" \
-H "x-api-key: $YEETPOST_API_KEY"path parameters
Id of the connection, as returned by GET /connections.
response 200
The queue, in slot order.
headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset
response body
The posts waiting in this connection's queue, earliest slot first.
How many images the post carries.
{
"queue": [
{
"id": "6f1b0c8a-1f3d-4d3e-9a2b-2f5a1c9e7d10",
"text": "shipped the new api docs today",
"scheduledFor": "2026-09-07T06:00:00.000Z",
"mediaCount": 1
}
]
}errors
Malformed connection id.
Missing or invalid API key.
No connection of yours with that id. Somebody else's connection is a 404 too.
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.
Every code is on the errors page.
Set a connection's posting slots
Replaces the connection's slot list, its timezone, or both. Send at least one of them.
Slots are wall times on the connection's own clock: a slot of { "weekday": 1, "time": "09:00" } with timezone Europe/Helsinki is 09:00 in Helsinki, whatever the offset is that week. At most 50 slots, and no two the same.
Changing the slots does not move posts that are already queued. A queued post was scheduled the moment it was queued, so it keeps the time it was given; the new slots decide where the next queued post lands.
Rate limit: 60 requests per minute per API key, shared with GET /connections/{connectionId}/queue.
curl -X PATCH "https://api.yeetpost.com/api/v2/connections/3f2a1b4c-5d6e-4f70-8a9b-0c1d2e3f4a5b/queue" \
-H "x-api-key: $YEETPOST_API_KEY" \
-H "content-type: application/json" \
-d '{
"timezone": "Europe/Helsinki",
"slots": [
{
"weekday": 1,
"time": "09:00"
},
{
"weekday": 4,
"time": "17:30"
}
]
}'path parameters
Id of the connection, as returned by GET /connections.
request body (application/json)
The complete slot list, replacing whatever was there. Slots must be unique. Posts already queued keep the times they were given.
Day of the week the slot falls on. 0 is Sunday.
24 hour HH:MM wall time, read on the connection's own clock.
IANA zone the slots are read in, for example Europe/Helsinki.
response 200
The queue settings as they now stand.
headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset
response body
Day of the week the slot falls on. 0 is Sunday.
24 hour HH:MM wall time, read on the connection's own clock.
{
"connection": {
"id": "3f2a1b4c-5d6e-4f70-8a9b-0c1d2e3f4a5b",
"slug": "linkedin",
"timezone": "Europe/Helsinki",
"queueSlots": [
{
"weekday": 1,
"time": "09:00"
},
{
"weekday": 4,
"time": "17:30"
}
]
}
}errors
Malformed connection id, an empty body, a slot that is not { weekday: 0-6, time: "HH:MM" }, the same slot twice, more than 50 slots, or a timezone the zone database has never heard of.
Missing or invalid API key.
No connection of yours with that id. Somebody else's connection is a 404 too.
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.
Every code is on the errors page.
profiles
One profile per end customer, when you are building a platform on yeetpost.
Create a profile
Creates one end customer's profile. Connect accounts into it with the console, then post to them with profileId.
Rate limit: 60 requests per minute per API key.
curl -X POST "https://api.yeetpost.com/api/v2/profiles" \
-H "x-api-key: $YEETPOST_API_KEY" \
-H "content-type: application/json" \
-d '{
"name": "Acme Coffee",
"externalId": "cus_4821"
}'request body (application/json)
What you call this end customer.
Your own id for the same end customer. Unique across your account: a second profile with the same one is refused with profile_external_id_conflict.
response 200
The profile.
headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset
response body
The profile's id, which you send as profileId when posting and connecting.
What you call this end customer. Shown in the console, never sent to a platform.
Your own id for the same end customer, unique across your account. Null when you did not give one.
An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.
{
"profile": {
"id": "b2d1f0a3-5c6e-4f70-8a9b-0c1d2e3f4a5b",
"name": "Acme Coffee",
"externalId": "cus_4821",
"createdAt": "2026-08-29T09:24:11.412Z"
}
}errors
A body without a name, a name outside 1 to 100 characters, or an externalId outside 1 to 200 characters.
Missing or invalid API key.
You already have 1000 profiles, which is the most an account can have at once (limit_exceeded; a deleted profile does not count), or the key is scoped to one profile and cannot create them (forbidden).
Another profile of yours already carries this externalId.
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.
Every code is on the errors page.
List your profiles
Lists your profiles, newest first.
Rate limit: 60 requests per minute per API key.
curl "https://api.yeetpost.com/api/v2/profiles" \
-H "x-api-key: $YEETPOST_API_KEY"query parameters
How many profiles to return.
How many profiles to skip, for paging.
response 200
Your profiles.
headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset
response body
The profile's id, which you send as profileId when posting and connecting.
What you call this end customer. Shown in the console, never sent to a platform.
Your own id for the same end customer, unique across your account. Null when you did not give one.
An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.
{
"profiles": [
{
"id": "b2d1f0a3-5c6e-4f70-8a9b-0c1d2e3f4a5b",
"name": "Acme Coffee",
"externalId": "cus_4821",
"createdAt": "2026-08-29T09:24:11.412Z"
}
],
"limit": 25,
"offset": 0
}errors
A limit outside 1 to 100, or a negative offset.
Missing or invalid API key.
This API key is scoped to one profile, and the endpoint is outside that scope. Profiles are managed with an unscoped key; a scoped key may only read its own profile and connect accounts into it.
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.
Every code is on the errors page.
Get one profile
Reads one profile of yours.
Rate limit: 60 requests per minute per API key.
curl "https://api.yeetpost.com/api/v2/profiles/b2d1f0a3-5c6e-4f70-8a9b-0c1d2e3f4a5b" \
-H "x-api-key: $YEETPOST_API_KEY"path parameters
The profile's id.
response 200
The profile.
headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset
response body
The profile's id, which you send as profileId when posting and connecting.
What you call this end customer. Shown in the console, never sent to a platform.
Your own id for the same end customer, unique across your account. Null when you did not give one.
An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.
{
"profile": {
"id": "b2d1f0a3-5c6e-4f70-8a9b-0c1d2e3f4a5b",
"name": "Acme Coffee",
"externalId": "cus_4821",
"createdAt": "2026-08-29T09:24:11.412Z"
}
}errors
A malformed profile id, a name outside 1 to 100 characters, an externalId outside 1 to 200 characters, or a body that names neither.
Missing or invalid API key.
This API key is scoped to one profile, and the endpoint is outside that scope. Profiles are managed with an unscoped key; a scoped key may only read its own profile and connect accounts into it.
No profile of yours has 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.
Every code is on the errors page.
Rename a profile
Changes a profile's name, its external id, or both. Its connections are untouched.
Rate limit: 60 requests per minute per API key.
curl -X PATCH "https://api.yeetpost.com/api/v2/profiles/b2d1f0a3-5c6e-4f70-8a9b-0c1d2e3f4a5b" \
-H "x-api-key: $YEETPOST_API_KEY" \
-H "content-type: application/json" \
-d '{
"name": "Acme Coffee Roasters"
}'path parameters
The profile's id.
request body (application/json)
Null takes the external id off the profile.
response 200
The profile.
headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset
response body
The profile's id, which you send as profileId when posting and connecting.
What you call this end customer. Shown in the console, never sent to a platform.
Your own id for the same end customer, unique across your account. Null when you did not give one.
An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.
{
"profile": {
"id": "b2d1f0a3-5c6e-4f70-8a9b-0c1d2e3f4a5b",
"name": "Acme Coffee",
"externalId": "cus_4821",
"createdAt": "2026-08-29T09:24:11.412Z"
}
}errors
A malformed profile id, a name outside 1 to 100 characters, an externalId outside 1 to 200 characters, or a body that names neither.
Missing or invalid API key.
This API key is scoped to one profile, and the endpoint is outside that scope. Profiles are managed with an unscoped key; a scoped key may only read its own profile and connect accounts into it.
No profile of yours has that id.
Another profile of yours already carries this externalId.
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.
Every code is on the errors page.
Delete a profile
Deletes the profile and disconnects every account inside it: the credentials are deleted, the posts that already went out stay readable, and anything still waiting to be sent is cancelled. Your bill drops to the connections you have left.
Rate limit: 60 requests per minute per API key.
curl -X DELETE "https://api.yeetpost.com/api/v2/profiles/b2d1f0a3-5c6e-4f70-8a9b-0c1d2e3f4a5b" \
-H "x-api-key: $YEETPOST_API_KEY"path parameters
The profile's id.
response 200
The profile is gone.
headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset
response body
true
How many connections went with the profile.
{
"id": "b2d1f0a3-5c6e-4f70-8a9b-0c1d2e3f4a5b",
"deleted": true,
"removedConnections": 2
}errors
A malformed profile id, a name outside 1 to 100 characters, an externalId outside 1 to 200 characters, or a body that names neither.
Missing or invalid API key.
This API key is scoped to one profile, and the endpoint is outside that scope. Profiles are managed with an unscoped key; a scoped key may only read its own profile and connect accounts into it.
No profile of yours has 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.
Every code is on the errors page.
Get a connect URL for a profile
Answers with a hosted page you send your end customer to. They connect one of their own accounts into this profile there and come back to your redirectUrl with the connection on the query.
The link works once and lasts fifteen minutes. It carries the whole authorization: nobody signs in to yeetpost, and the page never shows your key, your email or anything about your other customers.
When an account was connected, the customer lands on your redirectUrl with connectSessionId, connectionId, profileId, slug and platform added to its query. When they cancelled or the platform refused, they land with connectSessionId and error (cancelled or connect_failed) instead, and no connection was made. Any query string your redirectUrl already carries is kept; these parameters are added to it.
A test key opens a session that offers the sandbox and nothing else, which is how you run the whole flow in CI without connecting anybody's account.
A key scoped to a profile may only open sessions for that one; another profile answers 403 forbidden. When the key carries a redirect allowlist, redirectUrl must be on one of its origins.
Rate limit: 60 requests per minute per API key.
curl -X POST "https://api.yeetpost.com/api/v2/profiles/b2d1f0a3-5c6e-4f70-8a9b-0c1d2e3f4a5b/connect" \
-H "x-api-key: $YEETPOST_API_KEY" \
-H "content-type: application/json" \
-d '{
"platform": "linkedin",
"redirectUrl": "https://acme.example.com/settings/social"
}'path parameters
The profile the new connection goes into.
request body (application/json)
The platform to connect. Leave it out and the end customer picks one on the page. A test key may only name sandbox.
linkedin | x | bluesky | mastodon | discord | telegram | sandbox
Where the end customer goes when the flow ends, either way. Must be an https URL on a public host with no fragment; plain http is accepted for localhost only. It is bound to the session here and used exactly as stored, so nothing in the hosted page's own query can change where they land. When the API key carries a redirect allowlist, the URL's scheme, host and port must match one of its origins exactly; a key with no allowlist accepts any public https URL.
Draws the hosted page with no yeetpost header or footer, for a popup with your own branding around it.
response 200
The hosted url, its expiry and the session id.
headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset
response body
The page to send your end customer to. It carries a single use token and this is the only time it is shown: only its hash is stored and it cannot be read back.
An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.
Comes back on the redirect and on the connection.created event, and is what GET /connect-sessions/{connectSessionId} answers for.
{
"url": "https://yeetpost.com/connect/8Qm2rXk9vT1sB4nE6yPzL0aJdHcWfUgR",
"expiresAt": "2026-08-29T09:39:11.412Z",
"connectSessionId": "7f3c9a10-2b4d-4e88-9f01-3a5b7c9d1e2f"
}errors
A malformed profile id, a redirectUrl that is not an https URL on a public host, a redirectUrl with a fragment, a redirectUrl outside the key's redirect allowlist, a platform yeetpost cannot connect, or a platform a test key cannot reach.
Missing or invalid API key.
This API key is scoped to one profile, and the endpoint is outside that scope. Profiles are managed with an unscoped key; a scoped key may only read its own profile and connect accounts into it.
No profile of yours has 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.
Every code is on the errors page.
Read a connect session
How a connect link ended, for when you would rather poll than read the query your customer came back on. Both say the same thing.
Rate limit: 60 requests per minute per API key.
curl "https://api.yeetpost.com/api/v2/connect-sessions/7f3c9a10-2b4d-4e88-9f01-3a5b7c9d1e2f" \
-H "x-api-key: $YEETPOST_API_KEY"path parameters
The id POST /profiles/{profileId}/connect answered with.
response 200
The connect session.
headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset
response body
The platform the link was opened for, null when the end customer picks one on the page.
linkedin | x | bluesky | mastodon | discord | telegram | sandbox | null
pending while the customer still has the link open, connected once an account came in, failed when they cancelled or the platform refused, expired when the fifteen minutes ran out with nothing happening.
pending | connected | failed | expired
The connection that was made, null until one is.
Why the customer came back without a connection: cancelled when they backed out, connect_failed when the platform refused. Null otherwise.
cancelled | connect_failed | null
An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.
An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.
{
"connectSession": {
"id": "7f3c9a10-2b4d-4e88-9f01-3a5b7c9d1e2f",
"profileId": "b2d1f0a3-5c6e-4f70-8a9b-0c1d2e3f4a5b",
"platform": null,
"status": "connected",
"connectionId": "c4e2a1b6-8d3f-4a71-b0c9-5e6f7a8b9c0d",
"error": null,
"redirectUrl": "https://acme.example.com/settings/social",
"headless": false,
"expiresAt": "2026-08-29T09:39:11.412Z",
"createdAt": "2026-08-29T09:24:11.412Z"
}
}errors
A malformed connect session id.
Missing or invalid API key.
No connect session of yours has 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.
Every code is on the errors page.
keys
Minting and revoking API keys, one per end customer or one per integration.
Mint a key for a profile
Mints an API key scoped to this profile and answers with it once. The key is what an integration acting for that one end customer holds: it reads and posts inside the profile and can reach nothing else, whatever the call says.
secret is the key itself and appears in this answer only. No API endpoint reads the secret back; the account owner can reveal any key in the console. Store it now, mint another, or go and look it up as yourself.
Only an unscoped key mints keys, and a key minted here is always scoped: there is no way to ask this endpoint for a key that reaches the account. A test key mints test keys only, so a CI run cannot produce something that publishes.
Rate limit: 60 requests per minute per API key, and 20 mints an hour per account. The second window is there because minting is the one call that makes more callers: every key it hands back carries a per-key budget of its own.
curl -X POST "https://api.yeetpost.com/api/v2/profiles/b2d1f0a3-5c6e-4f70-8a9b-0c1d2e3f4a5b/keys" \
-H "x-api-key: $YEETPOST_API_KEY" \
-H "content-type: application/json" \
-d '{
"name": "Acme integration",
"type": "live",
"redirectAllowlist": [
"https://acme.example.com"
]
}'path parameters
The profile's id.
request body (application/json)
What to call the key, so you can tell it apart later.
A live key publishes; a test key reaches sandbox connections only. It is spelled out rather than defaulted, because a key that can publish should not come from a field left out. A test key can only ask for test.
live | test
The origins this key's connect URLs may send an end customer back to, as bare origins such as https://acme.example.com. Normalized and deduped. Leave it out, or send an empty list, for any public https URL.
response 201
The key, and the secret it will never be answered with again.
headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset
response body
What you called the key. Shown in the console and in these listings, never sent anywhere else.
A test key reaches sandbox connections only.
live | test
The first characters of the key, which is all any answer after the first says about it. Enough to tell two keys apart, and useless on its own. It is stored when the key is minted, so listing keys never decrypts a stored secret.
The profile this key is scoped to. Null for a key that reaches the whole account; a key minted over the API always carries one.
The origins this key's connect URLs may send an end customer back to. Null for any public https URL.
An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.
The key itself. This is the only answer that carries it: no API endpoint reads the secret back; the account owner can reveal any key in the console.
{
"key": {
"id": "9c4e1d75-6b3a-4f28-9d10-7e5c2a8b4f31",
"name": "Acme integration",
"type": "live",
"prefix": "yp_secret_4f9a2c",
"profileId": "b2d1f0a3-5c6e-4f70-8a9b-0c1d2e3f4a5b",
"redirectAllowlist": [
"https://acme.example.com"
],
"createdAt": "2026-08-30T09:24:11.412Z",
"lastUsedAt": null,
"revokedAt": null
},
"secret": "yp_secret_4f9a2c8e1b70d5a3c96f2e4b8d017a539e4a1d80b3f75c2e6a09d4b18f3c7e25"
}errors
A malformed profile id, a name outside 1 to 80 characters, a type that is not live or test, or a redirect allowlist entry that is not a bare origin.
Missing or invalid API key.
The key is scoped to a profile and cannot manage keys (forbidden), a test key asked for a live key (forbidden), or the profile already has 50 keys of that type (limit_exceeded; a revoked key does not count).
No profile of yours has 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.
Every code is on the errors page.
List a profile's keys
Lists the keys scoped to this profile, oldest first. Keys are listed by their first characters; no API endpoint reads the secret back, and the account owner can reveal any key in the console.
A test key sees the test keys and no live ones, the same rule that stops it revoking a live key.
Rate limit: 60 requests per minute per API key.
curl "https://api.yeetpost.com/api/v2/profiles/b2d1f0a3-5c6e-4f70-8a9b-0c1d2e3f4a5b/keys" \
-H "x-api-key: $YEETPOST_API_KEY"path parameters
The profile's id.
query parameters
List the revoked keys too. Left out, only the keys that still authenticate are listed.
true | false
How many keys to return.
How many keys to skip, for paging.
response 200
The profile's keys.
headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset
response body
What you called the key. Shown in the console and in these listings, never sent anywhere else.
A test key reaches sandbox connections only.
live | test
The first characters of the key, which is all any answer after the first says about it. Enough to tell two keys apart, and useless on its own. It is stored when the key is minted, so listing keys never decrypts a stored secret.
The profile this key is scoped to. Null for a key that reaches the whole account; a key minted over the API always carries one.
The origins this key's connect URLs may send an end customer back to. Null for any public https URL.
An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.
{
"keys": [
{
"id": "9c4e1d75-6b3a-4f28-9d10-7e5c2a8b4f31",
"name": "Acme integration",
"type": "live",
"prefix": "yp_secret_4f9a2c",
"profileId": "b2d1f0a3-5c6e-4f70-8a9b-0c1d2e3f4a5b",
"redirectAllowlist": [
"https://acme.example.com"
],
"createdAt": "2026-08-30T09:24:11.412Z",
"lastUsedAt": null,
"revokedAt": null
}
],
"limit": 25,
"offset": 0
}errors
A malformed profile id, an includeRevoked that is not true or false, a limit outside 1 to 100, or a negative offset.
Missing or invalid API key.
The key is scoped to a profile, and managing keys is an account capability.
No profile of yours has 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.
Every code is on the errors page.
Revoke a profile's key
Revokes one key of this profile. The key stops authenticating immediately, on every surface at once, and the row stays so the key still lists with includeRevoked=true.
Revoking is idempotent: a key that was already revoked answers the same 200, and the time it was first revoked stays.
One thing outlives the key: a connect URL it already issued keeps working for the rest of its fifteen minutes. The link carries its own single-use token and was authorized when it was created, so revoking the key that made it does not close it.
A test key may revoke test keys only. A live key of yours answers 404 not_found to a test key, the same answer a key of somebody else's account gets, so test credentials cannot be used to probe for live key ids.
Rate limit: 60 requests per minute per API key.
curl -X DELETE "https://api.yeetpost.com/api/v2/profiles/b2d1f0a3-5c6e-4f70-8a9b-0c1d2e3f4a5b/keys/9c4e1d75-6b3a-4f28-9d10-7e5c2a8b4f31" \
-H "x-api-key: $YEETPOST_API_KEY"path parameters
The profile's id.
The key's id, from the answer that minted it or from a listing.
response 200
The key is revoked, and was already revoked if you asked twice.
headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset
response body
true
{
"id": "9c4e1d75-6b3a-4f28-9d10-7e5c2a8b4f31",
"revoked": true
}errors
A malformed profile id or key id.
Missing or invalid API key.
The key is scoped to a profile, and managing keys is an account capability.
No profile of yours has that id, that profile has no such key, or a test key asked for a live 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.
Every code is on the errors page.
List your keys
Lists your account's own keys, the unscoped ones and every profile's, oldest first, each with the profile it is scoped to. Keys are listed by their first characters; no API endpoint reads the secret back, and the account owner can reveal any key in the console.
Only an unscoped key may read this: a scoped key gets 403 forbidden rather than a listing of one. A test key sees the test keys and no live ones.
Rate limit: 60 requests per minute per API key.
curl "https://api.yeetpost.com/api/v2/keys" \
-H "x-api-key: $YEETPOST_API_KEY"query parameters
List the revoked keys too. Left out, only the keys that still authenticate are listed.
true | false
How many keys to return.
How many keys to skip, for paging.
response 200
Your keys.
headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset
response body
What you called the key. Shown in the console and in these listings, never sent anywhere else.
A test key reaches sandbox connections only.
live | test
The first characters of the key, which is all any answer after the first says about it. Enough to tell two keys apart, and useless on its own. It is stored when the key is minted, so listing keys never decrypts a stored secret.
The profile this key is scoped to. Null for a key that reaches the whole account; a key minted over the API always carries one.
The origins this key's connect URLs may send an end customer back to. Null for any public https URL.
An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.
{
"keys": [
{
"id": "1a7f0c62-8d34-4b91-8f27-6c5b3e2d9a10",
"name": "Server key",
"type": "live",
"prefix": "yp_secret_0b31de",
"profileId": null,
"redirectAllowlist": null,
"createdAt": "2026-08-12T11:02:44.108Z",
"lastUsedAt": "2026-08-30T09:24:11.412Z",
"revokedAt": null
},
{
"id": "9c4e1d75-6b3a-4f28-9d10-7e5c2a8b4f31",
"name": "Acme integration",
"type": "live",
"prefix": "yp_secret_4f9a2c",
"profileId": "b2d1f0a3-5c6e-4f70-8a9b-0c1d2e3f4a5b",
"redirectAllowlist": [
"https://acme.example.com"
],
"createdAt": "2026-08-30T09:24:11.412Z",
"lastUsedAt": null,
"revokedAt": null
}
],
"limit": 25,
"offset": 0
}errors
An includeRevoked that is not true or false, a limit outside 1 to 100, or a negative offset.
Missing or invalid API key.
The key is scoped to a profile, and managing keys is an account capability.
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.
Every code is on the errors page.
Revoke a key
Revokes any key of your account, scoped or not. The key stops authenticating immediately.
A key may revoke itself: the call answers 200, and the next request made with it is 401 unauthorized.
Revoking is idempotent: a key that was already revoked answers the same 200, and the time it was first revoked stays.
One thing outlives the key: a connect URL it already issued keeps working for the rest of its fifteen minutes. The link carries its own single-use token and was authorized when it was created, so revoking the key that made it does not close it.
A test key may revoke test keys only. A live key of yours answers 404 not_found to a test key, the same answer a key of somebody else's account gets, so test credentials cannot be used to probe for live key ids.
Rate limit: 60 requests per minute per API key.
curl -X DELETE "https://api.yeetpost.com/api/v2/keys/9c4e1d75-6b3a-4f28-9d10-7e5c2a8b4f31" \
-H "x-api-key: $YEETPOST_API_KEY"path parameters
The key's id, from the answer that minted it or from a listing.
response 200
The key is revoked, and was already revoked if you asked twice.
headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset
response body
true
{
"id": "9c4e1d75-6b3a-4f28-9d10-7e5c2a8b4f31",
"revoked": true
}errors
A malformed key id.
Missing or invalid API key.
The key is scoped to a profile, and managing keys is an account capability.
No key of yours has that id, or a test key asked for a live 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.
Every code is on the errors page.
media
Images you upload once and attach to posts. Free within your storage allowance.
Upload an image
Uploads one image and answers with the id you attach to a post. The body is the raw file, not a multipart form: set Content-Type to the image type and send the bytes. The bytes have to be that type: a file whose signature disagrees with the header is rejected with 400 invalid_media_type.
Images are free. The only limits are size: 8 MB per image, 50 MB stored per account, and 5 images per post. Delete images you no longer need to make room.
Uploading the same bytes twice is one image. The second upload answers with the id of the first and costs nothing against your allowance, so a client that retries an upload cannot fill your storage with copies. If the second upload carries alt_text and the stored image has none, the description is kept; if the stored image already has one, it wins and the new one is ignored. Give a post its own altText to override either.
Rate limit: 60 requests per minute per API key.
curl -X POST "https://api.yeetpost.com/api/v2/media" \
-H "x-api-key: $YEETPOST_API_KEY" \
-H "content-type: image/png" \
--data-binary @image.pngquery parameters
Description of the image for screen readers, kept with the upload and used by every post that attaches it unless the post gives its own.
The image itself. At most 8 MB.
response 200
The image is stored and ready to attach.
headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset
response body
Id to attach to a post. Uploading the same bytes again returns this same id.
image/png | image/jpeg | image/gif | image/webp
Size of the stored image in bytes.
{
"mediaId": "9b1c2d3e-4f50-4a6b-8c7d-1e2f3a4b5c6d",
"contentType": "image/png",
"sizeBytes": 48213
}errors
Empty body, a bad alt_text, or bytes that are not the image type they were sent as (invalid_media_type).
Missing or invalid API key.
The image is over 8 MB (media_too_large), or storing it would put you over your 50 MB allowance (media_storage_full).
The Content-Type is not one of the image types we take.
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.
Every code is on the errors page.
Download an image
Serves the bytes back, with the Content-Type they were uploaded as. The response also carries X-Content-Type-Options: nosniff and Content-Disposition: inline, so a browser renders it as the type it is and never guesses another. Only your own images: somebody else's id answers 404.
Rate limit: 60 requests per minute per API key.
curl "https://api.yeetpost.com/api/v2/media/9b1c2d3e-4f50-4a6b-8c7d-1e2f3a4b5c6d" \
-H "x-api-key: $YEETPOST_API_KEY"path parameters
Id of the image, as returned by POST /media.
response 200
The image itself.
headers: x-content-type-options, content-disposition, x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset
errors
Malformed media id.
Missing or invalid API key.
No image 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.
Every code is on the errors page.
Delete an image
Deletes the image and frees the storage. Posts that used it keep their text and lose the image, so delete an image only once the posts that need it have gone out. A scheduled post whose images were deleted sends as text.
Rate limit: 60 requests per minute per API key.
curl -X DELETE "https://api.yeetpost.com/api/v2/media/9b1c2d3e-4f50-4a6b-8c7d-1e2f3a4b5c6d" \
-H "x-api-key: $YEETPOST_API_KEY"path parameters
Id of the image, as returned by POST /media.
response 200
The image is deleted.
headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset
response body
true
{
"id": "9b1c2d3e-4f50-4a6b-8c7d-1e2f3a4b5c6d",
"deleted": true
}errors
Malformed media id.
Missing or invalid API key.
No image 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.
Every code is on the errors page.
posts
Publishing, scheduling and looking up posts.
Post plain text to one connection (simple variant)
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'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 and within 30 days. Without it the post goes out immediately.
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
Permalink to the published post.
Sent
sms | email | slack
true
Scheduled
true
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"
}errors
Empty body, bad scheduled_for, or unknown connection.
Missing or invalid API key.
Posting to X needs a payment method, or would go past your own monthly X spend cap.
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.
Every code is on the errors page.
Post to one or more connections
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.
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"
]
}'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 and within 30 days. 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 either this or media, never both.
Images to attach, each with optional alt text. 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.
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 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 and hyphens. Whitespace is trimmed 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.
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 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 and for scheduled posts.
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.
Why a post did not make it, or null when nothing went wrong.
Same vocabulary as the top-level error field.
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)."
}
}
]
}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.
Every code is on the errors page.
List your 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"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.
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.
headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset
response body
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.
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.
Why a post did not make it, or null when nothing went wrong.
Same vocabulary as the top-level error field.
Your own labels for the post. At most 10, each 1 to 40 characters of lowercase letters, digits and hyphens. Whitespace is trimmed 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.
{
"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
}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.
Every code is on the errors page.
Get a single post
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"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
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.
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.
Why a post did not make it, or null when nothing went wrong.
Same vocabulary as the top-level error field.
Your own labels for the post. At most 10, each 1 to 40 characters of lowercase letters, digits and hyphens. Whitespace is trimmed 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.
{
"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
}
}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.
Every code is on the errors page.
Cancel a scheduled post, delete a draft, or delete a published one
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 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"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.
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"
}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.
Every code is on the errors page.
Edit a draft
Edits a draft in place: its text, its shape, its images and its tags. Only what you send is written.
Only drafts can be edited. A post that is sent, scheduled, cancelled or failed is a record of what happened, so editing one is 400 invalid_request. Whatever the draft 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"
]
}'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.
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 draft 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 and hyphens. Whitespace is trimmed and repeats are dropped. Filter by them with GET /posts?tag=.
response 200
The draft as it now stands.
headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset
response body
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.
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.
Why a post did not make it, or null when nothing went wrong.
Same vocabulary as the top-level error field.
Your own labels for the post. At most 10, each 1 to 40 characters of lowercase letters, digits and hyphens. Whitespace is trimmed 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.
{
"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
}
}errors
Malformed post id or body, a shape or image the connection cannot take, or a post that is not a draft.
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.
Every code is on the errors page.
Publish a draft
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 and fair_use_reached. 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"
}'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 and within 30 days.
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 and for scheduled posts.
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.
Why a post did not make it, or null when nothing went wrong.
Same vocabulary as the top-level error field.
{
"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
}
}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.
Every code is on the errors page.
analytics
The numbers yeetpost reads back off the platforms after a post goes out.
Get a post's numbers
The numbers yeetpost has read back from the platform for one post: the newest read, and every read before it.
A post is read an hour after it goes out, then hourly for 48 hours, then once a day until it is 30 days old. Before the first read latest is null.
What each platform reports differs, and a number a platform does not report is null rather than 0. Bluesky and Mastodon give likes, reposts and replies for free. LinkedIn gives likes and comments on any share, and impressions and clicks only on a page. X gives impressions, likes, reposts and replies, and X charges for the read, so it is off unless the account has X analytics enabled.
Rate limit: 60 requests per minute per API key.
curl "https://api.yeetpost.com/api/v2/posts/6f1b0c8a-1f3d-4d3e-9a2b-2f5a1c9e7d10/metrics" \
-H "x-api-key: $YEETPOST_API_KEY"path parameters
Id of the post, as returned by POST /posts or GET /posts.
response 200
The post's numbers.
headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset
response body
The most recent read, null when the post has not been read yet. A post is first read an hour after it goes out.
Every read, oldest first. A post is read hourly for 48 hours and then daily until it is 30 days old, so a finished history is 78 reads.
An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.
How many times the post was shown. X and LinkedIn pages report it; Bluesky, Mastodon and personal LinkedIn profiles do not. Null means the platform does not report this number, which is not the same as zero.
Likes, favourites or reactions, whatever the platform calls them. Null means the platform does not report this number, which is not the same as zero.
Reposts, retweets or boosts. On Bluesky quotes are counted here too. LinkedIn does not report it. Null means the platform does not report this number, which is not the same as zero.
Replies or comments. Null means the platform does not report this number, which is not the same as zero.
Link clicks. LinkedIn pages report it; nobody else does. Null means the platform does not report this number, which is not the same as zero.
{
"latest": {
"fetchedAt": "2026-08-30T10:00:00.000Z",
"impressions": null,
"likes": 12,
"reposts": 3,
"replies": 1,
"clicks": null
},
"history": [
{
"fetchedAt": "2026-08-29T10:00:00.000Z",
"impressions": null,
"likes": 4,
"reposts": 1,
"replies": 0,
"clicks": null
},
{
"fetchedAt": "2026-08-30T10:00:00.000Z",
"impressions": null,
"likes": 12,
"reposts": 3,
"replies": 1,
"clicks": null
}
]
}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.
Every code is on the errors page.
Totals, a per-day series and a per-platform split
Every post that went out in a window, with the newest numbers read for each of them.
Counters are running totals, so a post counts once, at whatever its newest read said. A post counts on the day it went out, which is what makes days a series you can chart.
The window is named in whole UTC days at both ends. Left out, it is the last 30 days, which is exactly as long as a post's numbers are refreshed for. The longest window is 365 days.
Scoped like everything else: a test key counts sandbox posts only, and a key scoped to a profile counts that profile's posts only.
Rate limit: 60 requests per minute per API key.
curl "https://api.yeetpost.com/api/v2/analytics" \
-H "x-api-key: $YEETPOST_API_KEY"query parameters
First UTC day of the window, inclusive, as YYYY-MM-DD.
Last UTC day of the window, inclusive, as YYYY-MM-DD.
Count only the posts that went out through this profile's connections.
Count only the posts that went out to this platform, such as bluesky.
Count only the posts that went out through this connection.
response 200
The window.
headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset
response body
Posts that went out in the window.
One entry per UTC day of the window, gaps included, so it can be charted as it comes. A post counts on the day it went out, and its numbers count with it.
The same window split by platform. A platform with no posts in the window is not listed.
The platform, such as bluesky or mastodon.
First UTC day of the window, inclusive.
Last UTC day of the window, inclusive.
{
"totals": {
"posts": 2,
"impressions": 0,
"likes": 16,
"reposts": 4,
"replies": 1,
"clicks": 0
},
"days": [
{
"day": "2026-08-29",
"posts": 1,
"impressions": 0,
"likes": 4,
"reposts": 1,
"replies": 0
},
{
"day": "2026-08-30",
"posts": 1,
"impressions": 0,
"likes": 12,
"reposts": 3,
"replies": 1
}
],
"byPlatform": [
{
"platformId": "bluesky",
"posts": 2,
"impressions": 0,
"likes": 16,
"reposts": 4,
"replies": 1,
"clicks": 0
}
],
"from": "2026-08-29",
"to": "2026-08-30"
}errors
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.
Every code is on the errors page.
webhooks
Delivery of post.sent and post.failed events to your own https endpoint.
Register a webhook
Registers an https endpoint of yours to receive post.sent and post.failed events.
The URL must be https and must point at a public host: private, loopback, link-local and metadata addresses are rejected. Deliveries do not follow redirects, so register the final URL.
The signing secret is returned exactly once, in this response. Store it somewhere safe; there is no way to read it again, only to delete the webhook and register a new one.
Rate limit: 60 requests per minute per API key, shared with GET /webhooks.
curl -X POST "https://api.yeetpost.com/api/v2/webhooks" \
-H "x-api-key: $YEETPOST_API_KEY" \
-H "content-type: application/json" \
-d '{
"url": "https://example.com/hooks/yeetpost",
"events": [
"post.sent",
"post.failed"
]
}'request body (application/json)
Where to POST events. Must be https and must point at a public host.
Events to subscribe to. Defaults to all of them.
post.sent | post.failed | post.deleted | post.metrics | connection.created | connection.removed
response 200
The webhook was registered. This is the only response that contains the secret.
headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset
response body
post.sent | post.failed | post.deleted | post.metrics | connection.created | connection.removed
An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.
The signing secret, yp_whsec_…. Shown only here, only once.
{
"webhook": {
"id": "2c9a4f3b-77d1-4b0e-9f2a-8c3e1a5b7d90",
"url": "https://example.com/hooks/yeetpost",
"events": [
"post.sent",
"post.failed"
],
"secret": "yp_whsec_3f1a...",
"createdAt": "2026-08-26T09:24:11.412Z"
}
}errors
Missing url, a non-https url, a url pointing at a non-public host, or an unknown event name. 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.
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.
Every code is on the errors page.
List your webhooks
Your webhooks, newest first. Secrets are never included.
Rate limit: 60 requests per minute per API key, shared with POST /webhooks.
curl "https://api.yeetpost.com/api/v2/webhooks" \
-H "x-api-key: $YEETPOST_API_KEY"response 200
Your webhooks.
headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset
response body
post.sent | post.failed | post.deleted | post.metrics | connection.created | connection.removed
An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.
{
"webhooks": [
{
"id": "2c9a4f3b-77d1-4b0e-9f2a-8c3e1a5b7d90",
"url": "https://example.com/hooks/yeetpost",
"events": [
"post.sent",
"post.failed"
],
"createdAt": "2026-08-26T09:24:11.412Z"
}
]
}errors
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.
Every code is on the errors page.
Delete a webhook
Stops delivering events to this webhook. Deliveries that are still queued for it are marked failed.
Rate limit: 60 requests per minute per API key.
curl -X DELETE "https://api.yeetpost.com/api/v2/webhooks/2c9a4f3b-77d1-4b0e-9f2a-8c3e1a5b7d90" \
-H "x-api-key: $YEETPOST_API_KEY"path parameters
Id of the webhook, as returned by POST /webhooks or GET /webhooks.
response 200
The webhook is deleted.
headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset
response body
true
{
"id": "2c9a4f3b-77d1-4b0e-9f2a-8c3e1a5b7d90",
"deleted": true
}errors
Malformed webhook id.
Missing or invalid API key.
No webhook 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.
Every code is on the errors page.
List a webhook's deliveries
The delivery log for one webhook of yours, newest first: what was sent, how it went, and the last error when it did not. Use it to see why a receiver is not getting events.
Deliveries are kept for 30 days. A dead letter's 30 days run from when it died rather than from when it was queued, so a delivery that spent its last day retrying is still there to replay.
Rate limit: 60 requests per minute per API key.
curl "https://api.yeetpost.com/api/v2/webhooks/2c9a4f3b-77d1-4b0e-9f2a-8c3e1a5b7d90/deliveries" \
-H "x-api-key: $YEETPOST_API_KEY"path parameters
Id of the webhook, as returned by POST /webhooks or GET /webhooks.
query parameters
How many deliveries to return.
Only deliveries in this state.
pending | delivered | failed | dead
Id of the last delivery of the previous page. New deliveries land at the top of this list while you page it, so the next page is the deliveries older than that one rather than the ones past an offset.
response 200
The webhook's deliveries, newest first.
headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset
response body
The same id the delivery carries in its x-yeetpost-delivery header.
Id of the event, the same value as id. It is what the delivery carries in x-yeetpost-event-id and in the eventId field of its body, on every attempt and every replay. Deduplicate on it.
post.sent | post.failed | post.deleted | post.metrics | connection.created | connection.removed
pending is waiting for its next attempt, processing is being sent right now, failed means the webhook was deleted under it, dead ran out of attempts and is only sent again by a replay.
pending | processing | delivered | failed | dead
How many times it has been sent. Up to 7, and back to 0 after a replay.
An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.
An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.
Same format as Timestamp, or null.
Same format as Timestamp, or null.
The status your receiver answered the last attempt with, or null when the request never got a response.
Our own one-line summary of why the last attempt did not land, or null. Never your receiver's response body.
Whether there is another page. Ask for it with startingAfter set to the id of the last delivery here.
{
"deliveries": [
{
"id": "8d3f2b7c-4e1a-4c6d-9b0f-1a2b3c4d5e6f",
"eventId": "8d3f2b7c-4e1a-4c6d-9b0f-1a2b3c4d5e6f",
"event": "post.sent",
"status": "delivered",
"attempts": 1,
"createdAt": "2026-08-26T09:24:11.412Z",
"nextAttemptAt": "2026-08-26T09:24:11.412Z",
"deliveredAt": "2026-08-26T09:24:12.004Z",
"deadAt": null,
"lastStatusCode": 200,
"lastError": null
},
{
"id": "1c4e5f6a-7b8c-4d9e-8f0a-1b2c3d4e5f60",
"eventId": "1c4e5f6a-7b8c-4d9e-8f0a-1b2c3d4e5f60",
"event": "post.failed",
"status": "dead",
"attempts": 7,
"createdAt": "2026-08-25T18:02:44.118Z",
"nextAttemptAt": "2026-08-26T14:39:12.882Z",
"deliveredAt": null,
"deadAt": "2026-08-26T14:39:13.204Z",
"lastStatusCode": 500,
"lastError": "response status 500"
}
],
"limit": 25,
"hasMore": false
}errors
Malformed webhook id, a limit outside 1 to 100, an unknown status, or a startingAfter that is not a delivery of this webhook.
Missing or invalid API key.
No webhook 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.
Every code is on the errors page.
Replay a delivery
Sends one delivery again. It is a new attempt chain on the same event: the same eventId, the same body, the same signature rules, so a receiver that deduplicates on the event id sees the event it missed rather than a new one.
A dead, failed or delivered delivery can be replayed. One that is still pending or processing is refused: it is already on its way.
Rate limit: 60 requests per minute per API key.
curl -X POST "https://api.yeetpost.com/api/v2/webhooks/2c9a4f3b-77d1-4b0e-9f2a-8c3e1a5b7d90/deliveries/1c4e5f6a-7b8c-4d9e-8f0a-1b2c3d4e5f60/replay" \
-H "x-api-key: $YEETPOST_API_KEY"path parameters
Id of the webhook, as returned by POST /webhooks or GET /webhooks.
Id of the delivery, as returned by GET /webhooks/{webhookId}/deliveries.
response 200
The delivery is queued for a fresh attempt chain.
headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset
response body
One attempt series at delivering one event to one webhook.
The same id the delivery carries in its x-yeetpost-delivery header.
Id of the event, the same value as id. It is what the delivery carries in x-yeetpost-event-id and in the eventId field of its body, on every attempt and every replay. Deduplicate on it.
post.sent | post.failed | post.deleted | post.metrics | connection.created | connection.removed
pending is waiting for its next attempt, processing is being sent right now, failed means the webhook was deleted under it, dead ran out of attempts and is only sent again by a replay.
pending | processing | delivered | failed | dead
How many times it has been sent. Up to 7, and back to 0 after a replay.
An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.
An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.
Same format as Timestamp, or null.
Same format as Timestamp, or null.
The status your receiver answered the last attempt with, or null when the request never got a response.
Our own one-line summary of why the last attempt did not land, or null. Never your receiver's response body.
{
"delivery": {
"id": "1c4e5f6a-7b8c-4d9e-8f0a-1b2c3d4e5f60",
"eventId": "1c4e5f6a-7b8c-4d9e-8f0a-1b2c3d4e5f60",
"event": "post.failed",
"status": "pending",
"attempts": 0,
"createdAt": "2026-08-25T18:02:44.118Z",
"nextAttemptAt": "2026-08-27T08:11:02.551Z",
"deliveredAt": null,
"deadAt": null,
"lastStatusCode": null,
"lastError": null
}
}errors
Malformed ids, or a delivery that is already queued for another attempt.
Missing or invalid API key.
No webhook of yours with that id, or no delivery of that webhook 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.
Every code is on the errors page.
Send a test event
Queues a post.sent delivery with a fixed, obviously fake post in it and "test": true at the top level of the body, so you can check a receiver and its signature check without posting anything.
It goes out through the normal worker, signed and retried like any other delivery, and it shows up in the delivery log. The webhook gets it whatever it is subscribed to, because you asked for it by id.
Rate limit: 60 requests per minute per API key.
curl -X POST "https://api.yeetpost.com/api/v2/webhooks/2c9a4f3b-77d1-4b0e-9f2a-8c3e1a5b7d90/test" \
-H "x-api-key: $YEETPOST_API_KEY"path parameters
Id of the webhook, as returned by POST /webhooks or GET /webhooks.
response 200
The test delivery is queued.
headers: x-request-id, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset
response body
One attempt series at delivering one event to one webhook.
The same id the delivery carries in its x-yeetpost-delivery header.
Id of the event, the same value as id. It is what the delivery carries in x-yeetpost-event-id and in the eventId field of its body, on every attempt and every replay. Deduplicate on it.
post.sent | post.failed | post.deleted | post.metrics | connection.created | connection.removed
pending is waiting for its next attempt, processing is being sent right now, failed means the webhook was deleted under it, dead ran out of attempts and is only sent again by a replay.
pending | processing | delivered | failed | dead
How many times it has been sent. Up to 7, and back to 0 after a replay.
An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.
An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.
Same format as Timestamp, or null.
Same format as Timestamp, or null.
The status your receiver answered the last attempt with, or null when the request never got a response.
Our own one-line summary of why the last attempt did not land, or null. Never your receiver's response body.
{
"delivery": {
"id": "3a1d9c02-5b6e-4f7a-8c1d-9e0f1a2b3c4d",
"eventId": "3a1d9c02-5b6e-4f7a-8c1d-9e0f1a2b3c4d",
"event": "post.sent",
"status": "pending",
"attempts": 0,
"createdAt": "2026-08-27T08:11:02.551Z",
"nextAttemptAt": "2026-08-27T08:11:02.551Z",
"deliveredAt": null,
"deadAt": null,
"lastStatusCode": null,
"lastError": null
}
}errors
Malformed webhook id.
Missing or invalid API key.
No webhook 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.
Every code is on the errors page.
meta
The spec, and the health of the API.
Get the current health of the API
Reports whether the database answers and whether each background worker has finished a loop recently. A worker is ok when its last successful iteration is within three of its loop intervals, and never sooner than two minutes, so that a queue taking a minute to drain is not called an outage. status is degraded when any check is not ok. No API key needed. The answer is cached for 10 seconds, so time can be up to 10 seconds behind. Both health endpoints share one limit of 60 requests a minute per IP.
curl "https://api.yeetpost.com/api/v2/health"response 200
The health of the API. The status code is 200 whether the API is ok or degraded: read status.
response body
ok | degraded
How long the health query took.
One background worker loop. ok is false when the loop has not finished an iteration within three of its intervals, and when it has never finished one at all.
When the loop last ran, whether or not that iteration finished.
When the loop last finished an iteration.
One background worker loop. ok is false when the loop has not finished an iteration within three of its intervals, and when it has never finished one at all.
When the loop last ran, whether or not that iteration finished.
When the loop last finished an iteration.
One background worker loop. ok is false when the loop has not finished an iteration within three of its intervals, and when it has never finished one at all.
When the loop last ran, whether or not that iteration finished.
When the loop last finished an iteration.
The API version that answered.
When the health was measured, ISO 8601 UTC.
{
"status": "ok",
"checks": {
"database": {
"ok": true,
"latencyMs": 3
},
"scheduledPosts": {
"ok": true,
"lastRunAt": "2026-08-29T11:04:58.120Z",
"lastOkAt": "2026-08-29T11:04:58.120Z"
},
"webhookDeliveries": {
"ok": true,
"lastRunAt": "2026-08-29T11:04:57.870Z",
"lastOkAt": "2026-08-29T11:04:57.870Z"
},
"xUsageSweep": {
"ok": true,
"lastRunAt": "2026-08-29T11:04:31.004Z",
"lastOkAt": "2026-08-29T11:04:31.004Z"
}
},
"version": "2.0.0",
"time": "2026-08-29T11:05:00.512Z"
}Get daily uptime for the last 90 days
The health of the API is sampled once a minute and kept for 90 days. This endpoint aggregates those samples per UTC day. What is measured is minutes, not samples: a day owes 1440 of them, today owes the minutes already behind it, and a minute with no sample counts as degraded exactly like a sample that came back degraded. A day owes minutes only from the first sample ever taken onwards, so a day that was over before sampling began is unmeasured and comes back with uptimePercent null. Every day of the window is in the answer, so a day that was sampled and came back with nothing is 0 percent rather than missing. No API key needed, and the answer is cached for 60 seconds. Both health endpoints share one limit of 60 requests a minute per IP.
curl "https://api.yeetpost.com/api/v2/health/history"query parameters
How many days back to aggregate, 1 to 90.
response 200
One aggregate per UTC day, oldest first.
response body
The UTC day, YYYY-MM-DD.
Ok minutes over the minutes the day owed, to two decimals. Missing minutes count against it. Null when the day was over before the first sample was ever taken: nobody was watching it, so it is unmeasured rather than down.
How many of that day's minutes got a sample. A full day is 1440, and anything less is a stretch the API did not report in.
Minutes of that day that were not ok, the ones with no sample included. Zero for an unmeasured day.
{
"days": [
{
"day": "2026-08-27",
"uptimePercent": null,
"samples": 0,
"degradedMinutes": 0
},
{
"day": "2026-08-28",
"uptimePercent": 100,
"samples": 1440,
"degradedMinutes": 0
},
{
"day": "2026-08-29",
"uptimePercent": 99.72,
"samples": 718,
"degradedMinutes": 2
}
]
}errors
Invalid query parameters. This endpoint takes no API key, so it carries no request id or rate limit headers.
Every code is on the errors page.
Get public usage counts for the product
Public numbers about yeetpost as a whole, which is what the marketing site's proof pill draws. Nothing here is about one account: postsThisWeek and postsAllTime count posts that reached a platform (no error, not a draft, not taken down, not still waiting in a queue), sandbox connections and test-key traffic left out, and liveConnections counts the connections that can be posted to right now. The counts are whole numbers exactly as counted, so round them yourself. No API key needed. The answer is cached for 60 seconds, and this endpoint has its own limit of 60 requests a minute per IP.
curl "https://api.yeetpost.com/api/v2/stats"response 200
The public counts, and the platforms a post can go out to today.
response body
Posts that reached a platform in the last 7 days.
Posts that reached a platform, ever.
Connections that can be posted to right now, sandbox ones left out.
The platforms a post can go out to today.
When the counts were taken, ISO 8601 UTC.
{
"postsThisWeek": 1284,
"postsAllTime": 41903,
"liveConnections": 372,
"platformsLive": [
"linkedin",
"x",
"bluesky",
"discord",
"telegram",
"mastodon"
],
"time": "2026-08-30T11:05:00.512Z"
}Get this spec
Returns this OpenAPI document. No API key needed.
curl "https://api.yeetpost.com/api/v2/openapi.json"response 200
The OpenAPI 3.1 document for the yeetpost API.
Webhook events
What yeetpost sends to your endpoint. These are outbound: you implement them.
A post was published
Sent to every webhook of yours subscribed to post.sent when a post reaches its platform, whether it was posted immediately or on a schedule. Scheduling a post emits nothing on its own.
See the post.failed description for how to verify the signature; the rules are the same for both events.
delivery headers
The event name, the same value as event in the body.
Id of the event, the same value as eventId in the body. Stable across every retry and every replay of one event, so it is the key to deduplicate on.
The same id as x-yeetpost-event-id, under the name it had before event ids.
t=<unix seconds>,v1=<hex hmac-sha256 of "<t>.<raw body>">, keyed by your webhook secret.
test when the post behind the delivery was made with a test key, live otherwise. Key your receiver off this to keep sandbox traffic out of its real handling.
body
Id of this event, the same value as the x-yeetpost-event-id header. Every retry and every replay of one event carries the same id, so a receiver that remembers what it has handled cannot handle it twice.
post.sent | post.failed | post.deleted | post.metrics | connection.created | connection.removed
When the event was queued.
Only present, and only ever true, on the synthetic delivery POST /webhooks/{webhookId}/test queues. Nothing was posted.
true
sent | failed
Same format as Timestamp, or null.
An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.
How many images the post carried. The ids are left out on purpose: read the bytes back with GET /media/{mediaId} if you need them.
How many replies went out under the head post.
Whether the first comment landed.
Why a post did not make it, or null when nothing went wrong.
Same vocabulary as the top-level error field.
Your own labels for the post. At most 10, each 1 to 40 characters of lowercase letters, digits and hyphens. Whitespace is trimmed 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.
{
"eventId": "8d3f2b7c-4e1a-4c6d-9b0f-1a2b3c4d5e6f",
"event": "post.sent",
"timestamp": "2026-08-26T09:24:12.004Z",
"post": {
"id": "6f1b0c8a-1f3d-4d3e-9a2b-2f5a1c9e7d10",
"connectionSlug": "linkedin",
"text": "shipped the new api docs today",
"status": "sent",
"url": "https://www.linkedin.com/feed/update/urn:li:share:1234567890",
"scheduledFor": null,
"createdAt": "2026-08-26T09:24:11.412Z",
"error": null
}
}response 2XX
Any 2xx within 5 seconds counts as delivered. Anything else (or a timeout) is retried, up to 7 attempts in all, waiting 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and then 12 hours between them. After the seventh failure the delivery becomes a dead letter (status: dead) that is never retried, only replayed.
A post could not be published
Sent to every webhook of yours subscribed to post.failed when a post is rejected by the platform, blocked by your plan limit, or fails on its way out. post.error carries the reason.
Verifying the signature. Each delivery carries x-yeetpost-signature: t=<unix seconds>,v1=<hex hmac>. Compute HMAC-SHA256("<t>.<raw request body>") with your webhook secret as the key and compare it to v1 with a constant-time comparison. Sign the raw body bytes, before any JSON parsing or re-serialisation. Also reject deliveries whose t is more than about 5 minutes away from your clock, so a captured delivery cannot be replayed later.
delivery headers
The event name, the same value as event in the body.
Id of the event, the same value as eventId in the body. Stable across every retry and every replay of one event, so it is the key to deduplicate on.
The same id as x-yeetpost-event-id, under the name it had before event ids.
t=<unix seconds>,v1=<hex hmac-sha256 of "<t>.<raw body>">, keyed by your webhook secret.
test when the post behind the delivery was made with a test key, live otherwise. Key your receiver off this to keep sandbox traffic out of its real handling.
body
Id of this event, the same value as the x-yeetpost-event-id header. Every retry and every replay of one event carries the same id, so a receiver that remembers what it has handled cannot handle it twice.
post.sent | post.failed | post.deleted | post.metrics | connection.created | connection.removed
When the event was queued.
Only present, and only ever true, on the synthetic delivery POST /webhooks/{webhookId}/test queues. Nothing was posted.
true
sent | failed
Same format as Timestamp, or null.
An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.
How many images the post carried. The ids are left out on purpose: read the bytes back with GET /media/{mediaId} if you need them.
How many replies went out under the head post.
Whether the first comment landed.
Why a post did not make it, or null when nothing went wrong.
Same vocabulary as the top-level error field.
Your own labels for the post. At most 10, each 1 to 40 characters of lowercase letters, digits and hyphens. Whitespace is trimmed 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.
{
"eventId": "1c4e5f6a-7b8c-4d9e-8f0a-1b2c3d4e5f60",
"event": "post.failed",
"timestamp": "2026-08-26T09:24:12.004Z",
"post": {
"id": "0b7e4a52-9f0c-4a1c-8b3a-6d2f5b1c0e44",
"connectionSlug": "x",
"text": "shipped the new api docs today",
"status": "failed",
"url": null,
"scheduledFor": null,
"createdAt": "2026-08-26T09:24:11.412Z",
"error": {
"code": "platform_rejected",
"message": "X: duplicate post was detected"
}
}
}response 2XX
Any 2xx within 5 seconds counts as delivered. Anything else (or a timeout) is retried, up to 7 attempts in all, waiting 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and then 12 hours between them. After the seventh failure the delivery becomes a dead letter (status: dead) that is never retried, only replayed.
A post was deleted on its platform
Sent to every webhook of yours subscribed to post.deleted when DELETE /posts/{postId} takes a published post back down. Cancelling a scheduled post emits nothing: it was never published.
See the post.failed description for how to verify the signature; the rules are the same for every event.
delivery headers
The event name, the same value as event in the body.
Id of the event, the same value as eventId in the body. Stable across every retry and every replay of one event, so it is the key to deduplicate on.
The same id as x-yeetpost-event-id, under the name it had before event ids.
t=<unix seconds>,v1=<hex hmac-sha256 of "<t>.<raw body>">, keyed by your webhook secret.
test when the post behind the delivery was made with a test key, live otherwise. Key your receiver off this to keep sandbox traffic out of its real handling.
body
Id of this event, the same value as the x-yeetpost-event-id header.
post.deleted
When the event was queued.
The platform the post came down on, such as x or mastodon.
An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.
The profile the post's connection belongs to, null when it is one of your own accounts.
{
"eventId": "2b7c8d3f-4e1a-4c6d-9b0f-1a2b3c4d5e6f",
"event": "post.deleted",
"timestamp": "2026-08-29T11:02:44.204Z",
"post": {
"id": "6f1b0c8a-1f3d-4d3e-9a2b-2f5a1c9e7d10",
"connectionSlug": "linkedin",
"platformId": "linkedin",
"deletedAt": "2026-08-29T11:02:44.108Z"
}
}response 2XX
Any 2xx within 5 seconds counts as delivered. Anything else (or a timeout) is retried, up to 7 attempts in all, waiting 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and then 12 hours between them. After the seventh failure the delivery becomes a dead letter (status: dead) that is never retried, only replayed.
A day's reading of a post's numbers
Sent to every webhook of yours subscribed to post.metrics on the first read of each UTC day, for as long as the post is being refreshed. That is one delivery per post per day for 30 days, not one per hourly read.
A number the platform does not report is null rather than 0.
See the post.failed description for how to verify the signature; the rules are the same for every event.
delivery headers
The event name, the same value as event in the body.
Id of the event, the same value as eventId in the body. Stable across every retry and every replay of one event, so it is the key to deduplicate on.
The same id as x-yeetpost-event-id, under the name it had before event ids.
t=<unix seconds>,v1=<hex hmac-sha256 of "<t>.<raw body>">, keyed by your webhook secret.
test when the post behind the delivery was made with a test key, live otherwise. Key your receiver off this to keep sandbox traffic out of its real handling.
body
Id of this event, the same value as the x-yeetpost-event-id header.
post.metrics
When the event was queued.
The platform the numbers came from, such as x or mastodon.
The post's permalink.
The profile the post's connection belongs to, null when it is one of your own accounts.
One read of a post's numbers, as the platform reported them at that moment. Every counter is a running total, so the newest read is the post's numbers now.
An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.
How many times the post was shown. X and LinkedIn pages report it; Bluesky, Mastodon and personal LinkedIn profiles do not. Null means the platform does not report this number, which is not the same as zero.
Likes, favourites or reactions, whatever the platform calls them. Null means the platform does not report this number, which is not the same as zero.
Reposts, retweets or boosts. On Bluesky quotes are counted here too. LinkedIn does not report it. Null means the platform does not report this number, which is not the same as zero.
Replies or comments. Null means the platform does not report this number, which is not the same as zero.
Link clicks. LinkedIn pages report it; nobody else does. Null means the platform does not report this number, which is not the same as zero.
{
"eventId": "8d3f2b7c-4e1a-4c6d-9b0f-1a2b3c4d5e6f",
"event": "post.metrics",
"timestamp": "2026-08-30T10:00:00.120Z",
"post": {
"id": "6f1b0c8a-1f3d-4d3e-9a2b-2f5a1c9e7d10",
"connectionSlug": "bluesky",
"profileId": null,
"platformId": "bluesky",
"url": "https://bsky.app/profile/alex.bsky.social/post/3l2k"
},
"metrics": {
"fetchedAt": "2026-08-30T10:00:00.000Z",
"impressions": null,
"likes": 12,
"reposts": 3,
"replies": 1,
"clicks": null
}
}response 2XX
Any 2xx within 5 seconds counts as delivered. Anything else (or a timeout) is retried on the same schedule every other event uses.
An account was connected
Sent when an account is connected, whether it went into one of your profiles or into your own account. connection.id and connection.profileId together are the mapping to persist: the profile is your end customer, the connection is what you post to. Reconnecting an account that is already there is not a new connection and emits nothing.
delivery headers
The event name, the same value as event in the body.
Id of the event, the same value as eventId in the body. Stable across every retry and every replay of one event, so it is the key to deduplicate on.
The same id as x-yeetpost-event-id, under the name it had before event ids.
t=<unix seconds>,v1=<hex hmac-sha256 of "<t>.<raw body>">, keyed by your webhook secret.
test when the post behind the delivery was made with a test key, live otherwise. Key your receiver off this to keep sandbox traffic out of its real handling.
body
The delivery's id, the same value as the x-yeetpost-event-id header. Dedupe on it.
connection.created | connection.removed
An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.
The profile the connection belongs to, null when it is one of your own accounts. This pair of ids is the mapping to persist.
The connect session the account came in through, null when the connection was made in the console. Lets you file the connection against the customer you made the link for without waiting for their browser to come back.
{
"eventId": "1c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
"event": "connection.created",
"timestamp": "2026-08-29T09:24:12.004Z",
"connection": {
"id": "3f2a1b4c-5d6e-4f70-8a9b-0c1d2e3f4a5b",
"profileId": "b2d1f0a3-5c6e-4f70-8a9b-0c1d2e3f4a5b",
"connectSessionId": "7f3c9a10-2b4d-4e88-9f01-3a5b7c9d1e2f",
"platformId": "linkedin",
"slug": "linkedin"
}
}response 2XX
Any 2xx within 5 seconds counts as delivered. Anything else is retried on the same schedule the post events use.
An account was disconnected
Sent when a connection is disconnected, one at a time from the console or all at once by deleting the profile it sat in. The posts it already published stay readable; anything still waiting to go out is cancelled.
delivery headers
The event name, the same value as event in the body.
Id of the event, the same value as eventId in the body. Stable across every retry and every replay of one event, so it is the key to deduplicate on.
The same id as x-yeetpost-event-id, under the name it had before event ids.
t=<unix seconds>,v1=<hex hmac-sha256 of "<t>.<raw body>">, keyed by your webhook secret.
test when the post behind the delivery was made with a test key, live otherwise. Key your receiver off this to keep sandbox traffic out of its real handling.
body
The delivery's id, the same value as the x-yeetpost-event-id header. Dedupe on it.
connection.created | connection.removed
An ISO 8601 timestamp in UTC, in the same format as the timestamps you send in: 2026-08-26T09:24:11.412Z.
The profile the connection belongs to, null when it is one of your own accounts. This pair of ids is the mapping to persist.
The connect session the account came in through, null when the connection was made in the console. Lets you file the connection against the customer you made the link for without waiting for their browser to come back.
{
"eventId": "1c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
"event": "connection.removed",
"timestamp": "2026-08-29T09:24:12.004Z",
"connection": {
"id": "3f2a1b4c-5d6e-4f70-8a9b-0c1d2e3f4a5b",
"profileId": "b2d1f0a3-5c6e-4f70-8a9b-0c1d2e3f4a5b",
"connectSessionId": null,
"platformId": "linkedin",
"slug": "linkedin"
}
}response 2XX
Any 2xx within 5 seconds counts as delivered. Anything else is retried on the same schedule the post events use.