yeetpostyeetpost

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

Every endpoint here takes an API key, in 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.

GET/connections

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

profileIdstring <uuid>

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

connectionsobject[]required
connections[].idstring <uuid>required

The connection's id, which the queue endpoints take in their path.

connections[].slugstringrequired

The identifier you post to, for example linkedin or alexdoe_x.

connections[].platformIdstringrequired

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

connections[].descriptionstringrequired

Human-readable label for the account or channel behind the slug.

connections[].timezonestringrequired

IANA zone the posting slots are read in, for example Europe/Helsinki. UTC until you set one.

connections[].queueSlotsobject[]required

The connection's posting slots, in the order they were set. Empty when the connection has no queue.

connections[].queueSlots[].weekdayinteger, 0 to 6required

Day of the week the slot falls on. 0 is Sunday.

connections[].queueSlots[].timestring, matches ^([01]\d|2[0-3]):[0-5]\d$required

24 hour HH:MM wall time, read on the connection's own clock.

connections[].profileIdstring <uuid> | nullrequired

The profile the connection belongs to. Null when it is one of your own accounts rather than an end customer's.

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

400invalid_request

A profileId that is not a valid profile id, or that is not one of your profiles.

401unauthorized

Missing or invalid API key.

429too many requests

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

500internal_server_error

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/connections/{connectionId}

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

connectionIdstring <uuid>required

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

connectionobjectrequired
connection.idstring <uuid>required

The connection's id, which the queue endpoints take in their path.

connection.slugstringrequired

The identifier you post to, for example linkedin or alexdoe_x.

connection.platformIdstringrequired

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

connection.descriptionstringrequired

Human-readable label for the account or channel behind the slug.

connection.timezonestringrequired

IANA zone the posting slots are read in, for example Europe/Helsinki. UTC until you set one.

connection.queueSlotsobject[]required

The connection's posting slots, in the order they were set. Empty when the connection has no queue.

connection.queueSlots[].weekdayinteger, 0 to 6required

Day of the week the slot falls on. 0 is Sunday.

connection.queueSlots[].timestring, matches ^([01]\d|2[0-3]):[0-5]\d$required

24 hour HH:MM wall time, read on the connection's own clock.

connection.profileIdstring <uuid> | nullrequired

The profile the connection belongs to. Null when it is one of your own accounts rather than an end customer's.

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

400invalid_request

Malformed connection id.

401unauthorized

Missing or invalid API key.

404not_found

No connection of yours with that id. Somebody else's connection is a 404 too.

429too many requests

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

500internal_server_error

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/connections/{connectionId}/queue

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

connectionIdstring <uuid>required

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

queueobject[]required

The posts waiting in this connection's queue, earliest slot first.

queue[].idstring <uuid>required
queue[].textstringrequired
queue[].scheduledForobjectrequired
queue[].mediaCountintegerrequired

How many images the post carries.

response
{
  "queue": [
    {
      "id": "6f1b0c8a-1f3d-4d3e-9a2b-2f5a1c9e7d10",
      "text": "shipped the new api docs today",
      "scheduledFor": "2026-09-07T06:00:00.000Z",
      "mediaCount": 1
    }
  ]
}

errors

400invalid_request

Malformed connection id.

401unauthorized

Missing or invalid API key.

404not_found

No connection of yours with that id. Somebody else's connection is a 404 too.

429too many requests

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

500internal_server_error

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.

PATCH/connections/{connectionId}/queue

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

connectionIdstring <uuid>required

Id of the connection, as returned by GET /connections.

request body (application/json)

slotsobject[], at most 50 items

The complete slot list, replacing whatever was there. Slots must be unique. Posts already queued keep the times they were given.

slots[].weekdayinteger, 0 to 6required

Day of the week the slot falls on. 0 is Sunday.

slots[].timestring, matches ^([01]\d|2[0-3]):[0-5]\d$required

24 hour HH:MM wall time, read on the connection's own clock.

timezonestring

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

connectionobjectrequired
connection.idstring <uuid>required
connection.slugstringrequired
connection.timezonestringrequired
connection.queueSlotsobject[]required
connection.queueSlots[].weekdayinteger, 0 to 6required

Day of the week the slot falls on. 0 is Sunday.

connection.queueSlots[].timestring, matches ^([01]\d|2[0-3]):[0-5]\d$required

24 hour HH:MM wall time, read on the connection's own clock.

response
{
  "connection": {
    "id": "3f2a1b4c-5d6e-4f70-8a9b-0c1d2e3f4a5b",
    "slug": "linkedin",
    "timezone": "Europe/Helsinki",
    "queueSlots": [
      {
        "weekday": 1,
        "time": "09:00"
      },
      {
        "weekday": 4,
        "time": "17:30"
      }
    ]
  }
}

errors

400invalid_request

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.

401unauthorized

Missing or invalid API key.

404not_found

No connection of yours with that id. Somebody else's connection is a 404 too.

429too many requests

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

500internal_server_error

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.

POST/profiles

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)

namestring, 1 to 100 charactersrequired

What you call this end customer.

externalIdstring, 1 to 200 characters

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

profileobjectrequired
profile.idstring <uuid>required

The profile's id, which you send as profileId when posting and connecting.

profile.namestringrequired

What you call this end customer. Shown in the console, never sent to a platform.

profile.externalIdstring | nullrequired

Your own id for the same end customer, unique across your account. Null when you did not give one.

profile.createdAtstring <date-time>required

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

response
{
  "profile": {
    "id": "b2d1f0a3-5c6e-4f70-8a9b-0c1d2e3f4a5b",
    "name": "Acme Coffee",
    "externalId": "cus_4821",
    "createdAt": "2026-08-29T09:24:11.412Z"
  }
}

errors

400invalid_request

A body without a name, a name outside 1 to 100 characters, or an externalId outside 1 to 200 characters.

401unauthorized

Missing or invalid API key.

403limit_exceeded

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

409profile_external_id_conflict

Another profile of yours already carries this externalId.

429too many requests

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

500internal_server_error

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/profiles

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

limitinteger, 1 to 100, default 25

How many profiles to return.

offsetinteger, min 0, default 0

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

profilesobject[]required
profiles[].idstring <uuid>required

The profile's id, which you send as profileId when posting and connecting.

profiles[].namestringrequired

What you call this end customer. Shown in the console, never sent to a platform.

profiles[].externalIdstring | nullrequired

Your own id for the same end customer, unique across your account. Null when you did not give one.

profiles[].createdAtstring <date-time>required

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

limitintegerrequired
offsetintegerrequired
response
{
  "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

400invalid_request

A limit outside 1 to 100, or a negative offset.

401unauthorized

Missing or invalid API key.

403forbidden

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.

429too many requests

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

500internal_server_error

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/profiles/{profileId}

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

profileIdstring <uuid>required

The profile's id.

response 200

The profile.

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

response body

profileobjectrequired
profile.idstring <uuid>required

The profile's id, which you send as profileId when posting and connecting.

profile.namestringrequired

What you call this end customer. Shown in the console, never sent to a platform.

profile.externalIdstring | nullrequired

Your own id for the same end customer, unique across your account. Null when you did not give one.

profile.createdAtstring <date-time>required

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

response
{
  "profile": {
    "id": "b2d1f0a3-5c6e-4f70-8a9b-0c1d2e3f4a5b",
    "name": "Acme Coffee",
    "externalId": "cus_4821",
    "createdAt": "2026-08-29T09:24:11.412Z"
  }
}

errors

400invalid_request

A malformed profile id, a name outside 1 to 100 characters, an externalId outside 1 to 200 characters, or a body that names neither.

401unauthorized

Missing or invalid API key.

403forbidden

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.

404not_found

No profile of yours has that id.

429too many requests

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

500internal_server_error

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.

PATCH/profiles/{profileId}

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

profileIdstring <uuid>required

The profile's id.

request body (application/json)

namestring, 1 to 100 characters
externalIdstring | null, 1 to 200 characters

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

profileobjectrequired
profile.idstring <uuid>required

The profile's id, which you send as profileId when posting and connecting.

profile.namestringrequired

What you call this end customer. Shown in the console, never sent to a platform.

profile.externalIdstring | nullrequired

Your own id for the same end customer, unique across your account. Null when you did not give one.

profile.createdAtstring <date-time>required

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

response
{
  "profile": {
    "id": "b2d1f0a3-5c6e-4f70-8a9b-0c1d2e3f4a5b",
    "name": "Acme Coffee",
    "externalId": "cus_4821",
    "createdAt": "2026-08-29T09:24:11.412Z"
  }
}

errors

400invalid_request

A malformed profile id, a name outside 1 to 100 characters, an externalId outside 1 to 200 characters, or a body that names neither.

401unauthorized

Missing or invalid API key.

403forbidden

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.

404not_found

No profile of yours has that id.

409profile_external_id_conflict

Another profile of yours already carries this externalId.

429too many requests

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

500internal_server_error

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/profiles/{profileId}

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

profileIdstring <uuid>required

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

idstring <uuid>required
deletedbooleanrequired

true

removedConnectionsintegerrequired

How many connections went with the profile.

response
{
  "id": "b2d1f0a3-5c6e-4f70-8a9b-0c1d2e3f4a5b",
  "deleted": true,
  "removedConnections": 2
}

errors

400invalid_request

A malformed profile id, a name outside 1 to 100 characters, an externalId outside 1 to 200 characters, or a body that names neither.

401unauthorized

Missing or invalid API key.

403forbidden

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.

404not_found

No profile of yours has that id.

429too many requests

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

500internal_server_error

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/profiles/{profileId}/connect

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

profileIdstring <uuid>required

The profile the new connection goes into.

request body (application/json)

platformstring

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

redirectUrlstring <uri>required

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.

headlessboolean

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

urlstring <uri>required

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.

expiresAtstring <date-time>required

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

connectSessionIdstring <uuid>required

Comes back on the redirect and on the connection.created event, and is what GET /connect-sessions/{connectSessionId} answers for.

response
{
  "url": "https://yeetpost.com/connect/8Qm2rXk9vT1sB4nE6yPzL0aJdHcWfUgR",
  "expiresAt": "2026-08-29T09:39:11.412Z",
  "connectSessionId": "7f3c9a10-2b4d-4e88-9f01-3a5b7c9d1e2f"
}

errors

400invalid_request

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.

401unauthorized

Missing or invalid API key.

403forbidden

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.

404not_found

No profile of yours has that id.

429too many requests

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

500internal_server_error

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/connect-sessions/{connectSessionId}

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

connectSessionIdstring <uuid>required

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

connectSessionobjectrequired
connectSession.idstring <uuid>required
connectSession.profileIdstring <uuid>required
connectSession.platformstring | nullrequired

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

connectSession.statusstringrequired

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

connectSession.connectionIdstring <uuid> | nullrequired

The connection that was made, null until one is.

connectSession.errorstring | nullrequired

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

connectSession.redirectUrlstring <uri>required
connectSession.headlessbooleanrequired
connectSession.expiresAtstring <date-time>required

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

connectSession.createdAtstring <date-time>required

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

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

400invalid_request

A malformed connect session id.

401unauthorized

Missing or invalid API key.

404not_found

No connect session of yours has that id.

429too many requests

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

500internal_server_error

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.

POST/profiles/{profileId}/keys

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

profileIdstring <uuid>required

The profile's id.

request body (application/json)

namestring, 1 to 80 charactersrequired

What to call the key, so you can tell it apart later.

typestringrequired

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

redirectAllowliststring[], at most 20 items

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

keyobjectrequired
key.idstring <uuid>required
key.namestringrequired

What you called the key. Shown in the console and in these listings, never sent anywhere else.

key.typestringrequired

A test key reaches sandbox connections only.

live | test

key.prefixstringrequired

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.

key.profileIdstring <uuid> | nullrequired

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.

key.redirectAllowliststring[] | nullrequired

The origins this key's connect URLs may send an end customer back to. Null for any public https URL.

key.createdAtstring <date-time>required

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

key.lastUsedAtobjectrequired
key.revokedAtobjectrequired
secretstringrequired

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.

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

400invalid_request

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.

401unauthorized

Missing or invalid API key.

403limit_exceeded

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

404not_found

No profile of yours has that id.

429too many requests

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

500internal_server_error

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/profiles/{profileId}/keys

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

profileIdstring <uuid>required

The profile's id.

query parameters

includeRevokedstring

List the revoked keys too. Left out, only the keys that still authenticate are listed.

true | false

limitinteger, 1 to 100, default 25

How many keys to return.

offsetinteger, min 0, default 0

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

keysobject[]required
keys[].idstring <uuid>required
keys[].namestringrequired

What you called the key. Shown in the console and in these listings, never sent anywhere else.

keys[].typestringrequired

A test key reaches sandbox connections only.

live | test

keys[].prefixstringrequired

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.

keys[].profileIdstring <uuid> | nullrequired

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.

keys[].redirectAllowliststring[] | nullrequired

The origins this key's connect URLs may send an end customer back to. Null for any public https URL.

keys[].createdAtstring <date-time>required

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

keys[].lastUsedAtobjectrequired
keys[].revokedAtobjectrequired
limitintegerrequired
offsetintegerrequired
response
{
  "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

400invalid_request

A malformed profile id, an includeRevoked that is not true or false, a limit outside 1 to 100, or a negative offset.

401unauthorized

Missing or invalid API key.

403forbidden

The key is scoped to a profile, and managing keys is an account capability.

404not_found

No profile of yours has that id.

429too many requests

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

500internal_server_error

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/profiles/{profileId}/keys/{keyId}

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

profileIdstring <uuid>required

The profile's id.

keyIdstring <uuid>required

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

idstring <uuid>required
revokedbooleanrequired

true

response
{
  "id": "9c4e1d75-6b3a-4f28-9d10-7e5c2a8b4f31",
  "revoked": true
}

errors

400invalid_request

A malformed profile id or key id.

401unauthorized

Missing or invalid API key.

403forbidden

The key is scoped to a profile, and managing keys is an account capability.

404not_found

No profile of yours has that id, that profile has no such key, or a test key asked for a live key.

429too many requests

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

500internal_server_error

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/keys

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

includeRevokedstring

List the revoked keys too. Left out, only the keys that still authenticate are listed.

true | false

limitinteger, 1 to 100, default 25

How many keys to return.

offsetinteger, min 0, default 0

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

keysobject[]required
keys[].idstring <uuid>required
keys[].namestringrequired

What you called the key. Shown in the console and in these listings, never sent anywhere else.

keys[].typestringrequired

A test key reaches sandbox connections only.

live | test

keys[].prefixstringrequired

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.

keys[].profileIdstring <uuid> | nullrequired

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.

keys[].redirectAllowliststring[] | nullrequired

The origins this key's connect URLs may send an end customer back to. Null for any public https URL.

keys[].createdAtstring <date-time>required

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

keys[].lastUsedAtobjectrequired
keys[].revokedAtobjectrequired
limitintegerrequired
offsetintegerrequired
response
{
  "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

400invalid_request

An includeRevoked that is not true or false, a limit outside 1 to 100, or a negative offset.

401unauthorized

Missing or invalid API key.

403forbidden

The key is scoped to a profile, and managing keys is an account capability.

429too many requests

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

500internal_server_error

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/keys/{keyId}

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

keyIdstring <uuid>required

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

idstring <uuid>required
revokedbooleanrequired

true

response
{
  "id": "9c4e1d75-6b3a-4f28-9d10-7e5c2a8b4f31",
  "revoked": true
}

errors

400invalid_request

A malformed key id.

401unauthorized

Missing or invalid API key.

403forbidden

The key is scoped to a profile, and managing keys is an account capability.

404not_found

No key of yours has that id, or a test key asked for a live key.

429too many requests

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

500internal_server_error

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.

POST/media

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

query parameters

alt_textstring, at most 1000 characters

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

mediaIdstring <uuid>required

Id to attach to a post. Uploading the same bytes again returns this same id.

contentTypestringrequired

image/png | image/jpeg | image/gif | image/webp

sizeBytesintegerrequired

Size of the stored image in bytes.

response
{
  "mediaId": "9b1c2d3e-4f50-4a6b-8c7d-1e2f3a4b5c6d",
  "contentType": "image/png",
  "sizeBytes": 48213
}

errors

400invalid_request, invalid_media_type

Empty body, a bad alt_text, or bytes that are not the image type they were sent as (invalid_media_type).

401unauthorized

Missing or invalid API key.

413media_too_large, media_storage_full

The image is over 8 MB (media_too_large), or storing it would put you over your 50 MB allowance (media_storage_full).

415unsupported_content_type

The Content-Type is not one of the image types we take.

429too many requests

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

500internal_server_error

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/media/{mediaId}

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

mediaIdstring <uuid>required

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

400invalid_request

Malformed media id.

401unauthorized

Missing or invalid API key.

404not_found

No image of yours with that id.

429too many requests

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

500internal_server_error

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/media/{mediaId}

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

mediaIdstring <uuid>required

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

idstring <uuid>required
deletedbooleanrequired

true

response
{
  "id": "9b1c2d3e-4f50-4a6b-8c7d-1e2f3a4b5c6d",
  "deleted": true
}

errors

400invalid_request

Malformed media id.

401unauthorized

Missing or invalid API key.

404not_found

No image of yours with that id.

429too many requests

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

500internal_server_error

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/post/{connectionSlug}

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

connectionSlugstringrequired

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

query parameters

scheduled_forstring <date-time>

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

platformstringrequired

linkedin | x | bluesky | discord | telegram | mastodon

linkstringrequired

Permalink to the published post.

Sent

platformstringrequired

sms | email | slack

sentbooleanrequired

true

Scheduled

scheduledbooleanrequired

true

scheduled_forstring <date-time>required

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

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

Sent to SMS, email or Slack

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

Scheduled for later

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

errors

400invalid_request, invalid_connection

Empty body, bad scheduled_for, or unknown connection.

401unauthorized

Missing or invalid API key.

402payment_method_required, spend_cap_reached

Posting to X needs a payment method, or would go past your own monthly X spend cap.

403limit_exceeded, fair_use_reached

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.

415unsupported_content_type

Unsupported Content-Type.

422platform_rejected

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

429too many requests

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

500internal_server_error

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/posts

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

Idempotency-Keystring, 1 to 255 characters

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

request body (application/json)

textstring, at least 1 characterrequired

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

connectionSlugsstring[], at least 1 itemrequired

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

scheduledForstring <date-time>

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

timezonestring

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.

queueboolean

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

mediaIdsstring <uuid>[], at most 5 items

Ids of images to attach, from POST /media. Shorthand for media when no alt text is needed. Send either this or media, never both.

mediaobject[], at most 5 items

Images to attach, each with optional alt text. Send either this or mediaIds, never both.

media[].idstring <uuid>required

Media id from POST /media.

media[].altTextstring, at most 1000 characters

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.

threadstring[], at most 24 items

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

firstCommentstring, at least 1 character

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

isDraftboolean

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

tagsstring[], at most 10 items

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

profileIdstring <uuid>

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

resultsobject[]required

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

results[].connectionSlugstringrequired
results[].statusstringrequired

sent | scheduled | draft | failed

results[].postIdstring <uuid> | nullrequired

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

results[].urlstring | nullrequired

Permalink, for platforms that return one. null for SMS, email, Slack and for scheduled posts.

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

When this post is due, for scheduled items.

results[].threadCountintegerrequired

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

results[].hasFirstCommentbooleanrequired

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

results[].errorobject | nullrequired

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

results[].error.codestringrequired

Same vocabulary as the top-level error field.

results[].error.messagestringrequired

One connection sent, one refused by the platform

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

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

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

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

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

An image for X on a connection made before image posting

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

400invalid_request, invalid_connection

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.

401unauthorized

Missing or invalid API key.

409idempotency_key_conflict

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

429too many requests

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

500internal_server_error

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/posts

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

statusstring

Only return posts in this status.

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

tagstring[]

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

profileIdstring <uuid>

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

limitinteger, 1 to 100, default 25

How many posts to return.

offsetinteger, min 0, default 0

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

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

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

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

posts[].urlstring | nullrequired

Permalink, for platforms that return one.

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

Same format as Timestamp, or null.

posts[].createdAtstring <date-time>required

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

posts[].platformPostIdstring | nullrequired

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

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

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

posts[].errorobject | nullrequired

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

posts[].error.codestringrequired

Same vocabulary as the top-level error field.

posts[].error.messagestringrequired
posts[].tagsstring[], at most 10 itemsrequired

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

posts[].profileIdstring <uuid> | nullrequired

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

limitintegerrequired
offsetintegerrequired
response
{
  "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

400invalid_request

Invalid query parameters.

401unauthorized

Missing or invalid API key.

429too many requests

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

500internal_server_error

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/posts/{postId}

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

postIdstring <uuid>required

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

response 200

The post.

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

response body

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

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

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

post.urlstring | nullrequired

Permalink, for platforms that return one.

post.scheduledForstring <date-time> | nullrequired

Same format as Timestamp, or null.

post.createdAtstring <date-time>required

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

post.platformPostIdstring | nullrequired

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

post.deletedAtstring <date-time> | nullrequired

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

post.errorobject | nullrequired

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

post.error.codestringrequired

Same vocabulary as the top-level error field.

post.error.messagestringrequired
post.tagsstring[], at most 10 itemsrequired

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

post.profileIdstring <uuid> | nullrequired

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

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

400invalid_request

The post id is not a valid id.

401unauthorized

Missing or invalid API key.

404not_found

No post of yours with that id.

429too many requests

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

500internal_server_error

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/posts/{postId}

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

postIdstring <uuid>required

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

response 200

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

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

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

Cancelled scheduled post

idstring <uuid>required
statusstringrequired

cancelled

Deleted draft

idstring <uuid>required
deletedbooleanrequired

true

Deleted published post

idstring <uuid>required
statusstringrequired

deleted

deletedAtstring <date-time>required

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

A scheduled post that had not been sent yet

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

A draft, deleted for good

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

A published post, taken down on its platform

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

errors

400delete_unsupported

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

401unauthorized

Missing or invalid API key.

404not_found

No post of yours with that id.

409post_not_cancellable

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

422platform_rejected

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

429too many requests

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

500internal_server_error

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.

PATCH/posts/{postId}

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

postIdstring <uuid>required

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

request body (application/json)

textstring, at least 1 character

The new text. Leading and trailing whitespace is trimmed.

threadstring[], at most 24 items

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

firstCommentstring | null, at least 1 character

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

mediaIdsstring <uuid>[], at most 5 items

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.

mediaobject[], at most 5 items

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

media[].idstring <uuid>required

Media id from POST /media.

media[].altTextstring, at most 1000 characters

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

tagsstring[], at most 10 items

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

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

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

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

post.urlstring | nullrequired

Permalink, for platforms that return one.

post.scheduledForstring <date-time> | nullrequired

Same format as Timestamp, or null.

post.createdAtstring <date-time>required

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

post.platformPostIdstring | nullrequired

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

post.deletedAtstring <date-time> | nullrequired

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

post.errorobject | nullrequired

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

post.error.codestringrequired

Same vocabulary as the top-level error field.

post.error.messagestringrequired
post.tagsstring[], at most 10 itemsrequired

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

post.profileIdstring <uuid> | nullrequired

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

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

400invalid_request, invalid_media, media_unsupported, shape_unsupported

Malformed post id or body, a shape or image the connection cannot take, or a post that is not a draft.

401unauthorized

Missing or invalid API key.

404not_found

No post of yours with that id.

429too many requests

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

500internal_server_error

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/posts/{postId}/publish

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

postIdstring <uuid>required

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

header parameters

Idempotency-Keystring, 1 to 255 characters

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

request body (application/json)

scheduledForstring <date-time>

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

timezonestring

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

queueboolean

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

response 200

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

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

response body

resultobjectrequired
result.connectionSlugstringrequired
result.statusstringrequired

sent | scheduled | draft | failed

result.postIdstring <uuid> | nullrequired

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

result.urlstring | nullrequired

Permalink, for platforms that return one. null for SMS, email, Slack and for scheduled posts.

result.scheduledForstring <date-time> | nullrequired

When this post is due, for scheduled items.

result.threadCountintegerrequired

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

result.hasFirstCommentbooleanrequired

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

result.errorobject | nullrequired

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

result.error.codestringrequired

Same vocabulary as the top-level error field.

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

400invalid_request

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

401unauthorized

Missing or invalid API key.

404not_found

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

409idempotency_key_conflict

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

429too many requests

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

500internal_server_error

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/posts/{postId}/metrics

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

postIdstring <uuid>required

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

latestany | nullrequired

The most recent read, null when the post has not been read yet. A post is first read an hour after it goes out.

historyobject[]required

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.

history[].fetchedAtstring <date-time>required

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

history[].impressionsinteger | nullrequired

How many times the post was shown. X and LinkedIn pages report it; Bluesky, Mastodon and personal LinkedIn profiles do not. Null means the platform does not report this number, which is not the same as zero.

history[].likesinteger | nullrequired

Likes, favourites or reactions, whatever the platform calls them. Null means the platform does not report this number, which is not the same as zero.

history[].repostsinteger | nullrequired

Reposts, retweets or boosts. On Bluesky quotes are counted here too. LinkedIn does not report it. Null means the platform does not report this number, which is not the same as zero.

history[].repliesinteger | nullrequired

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

history[].clicksinteger | nullrequired

Link clicks. LinkedIn pages report it; nobody else does. Null means the platform does not report this number, which is not the same as zero.

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

400invalid_request

The post id is not a valid id.

401unauthorized

Missing or invalid API key.

404not_found

No post of yours with that id.

429too many requests

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

500internal_server_error

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/analytics

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

fromstring <date>

First UTC day of the window, inclusive, as YYYY-MM-DD.

tostring <date>

Last UTC day of the window, inclusive, as YYYY-MM-DD.

profileIdstring <uuid>

Count only the posts that went out through this profile's connections.

platformstring

Count only the posts that went out to this platform, such as bluesky.

connectionSlugstring

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

totalsobjectrequired
totals.postsintegerrequired

Posts that went out in the window.

totals.impressionsintegerrequired
totals.likesintegerrequired
totals.repostsintegerrequired
totals.repliesintegerrequired
totals.clicksintegerrequired
daysobject[]required

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.

days[].daystringrequired
days[].postsintegerrequired
days[].impressionsintegerrequired
days[].likesintegerrequired
days[].repostsintegerrequired
days[].repliesintegerrequired
byPlatformobject[]required

The same window split by platform. A platform with no posts in the window is not listed.

byPlatform[].platformIdstringrequired

The platform, such as bluesky or mastodon.

byPlatform[].postsintegerrequired
byPlatform[].impressionsintegerrequired
byPlatform[].likesintegerrequired
byPlatform[].repostsintegerrequired
byPlatform[].repliesintegerrequired
byPlatform[].clicksintegerrequired
fromstringrequired

First UTC day of the window, inclusive.

tostringrequired

Last UTC day of the window, inclusive.

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

400
401unauthorized

Missing or invalid API key.

429too many requests

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

500internal_server_error

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.

POST/webhooks

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)

urlstring <uri>required

Where to POST events. Must be https and must point at a public host.

eventsstring[], at least 1 item

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

webhookobjectrequired
webhook.idstring <uuid>required
webhook.urlstring <uri>required
webhook.eventsstring[]required

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

webhook.createdAtstring <date-time>required

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

webhook.secretstringrequired

The signing secret, yp_whsec_…. Shown only here, only once.

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

400invalid_request

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.

401unauthorized

Missing or invalid API key.

429too many requests

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

500internal_server_error

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/webhooks

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

webhooksobject[]required
webhooks[].idstring <uuid>required
webhooks[].urlstring <uri>required
webhooks[].eventsstring[]required

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

webhooks[].createdAtstring <date-time>required

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

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

401unauthorized

Missing or invalid API key.

429too many requests

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

500internal_server_error

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/webhooks/{webhookId}

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

webhookIdstring <uuid>required

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

idstring <uuid>required
deletedbooleanrequired

true

response
{
  "id": "2c9a4f3b-77d1-4b0e-9f2a-8c3e1a5b7d90",
  "deleted": true
}

errors

400invalid_request

Malformed webhook id.

401unauthorized

Missing or invalid API key.

404not_found

No webhook of yours with that id.

429too many requests

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

500internal_server_error

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/webhooks/{webhookId}/deliveries

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

webhookIdstring <uuid>required

Id of the webhook, as returned by POST /webhooks or GET /webhooks.

query parameters

limitinteger, 1 to 100, default 25

How many deliveries to return.

statusstring

Only deliveries in this state.

pending | delivered | failed | dead

startingAfterstring <uuid>

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

deliveriesobject[]required
deliveries[].idstring <uuid>required

The same id the delivery carries in its x-yeetpost-delivery header.

deliveries[].eventIdstring <uuid>required

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.

deliveries[].eventstringrequired

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

deliveries[].statusstringrequired

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

deliveries[].attemptsintegerrequired

How many times it has been sent. Up to 7, and back to 0 after a replay.

deliveries[].createdAtstring <date-time>required

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

deliveries[].nextAttemptAtstring <date-time>required

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

deliveries[].deliveredAtstring <date-time> | nullrequired

Same format as Timestamp, or null.

deliveries[].deadAtstring <date-time> | nullrequired

Same format as Timestamp, or null.

deliveries[].lastStatusCodeinteger | nullrequired

The status your receiver answered the last attempt with, or null when the request never got a response.

deliveries[].lastErrorstring | nullrequired

Our own one-line summary of why the last attempt did not land, or null. Never your receiver's response body.

limitintegerrequired
hasMorebooleanrequired

Whether there is another page. Ask for it with startingAfter set to the id of the last delivery here.

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

400invalid_request

Malformed webhook id, a limit outside 1 to 100, an unknown status, or a startingAfter that is not a delivery of this webhook.

401unauthorized

Missing or invalid API key.

404not_found

No webhook of yours with that id.

429too many requests

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

500internal_server_error

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/webhooks/{webhookId}/deliveries/{deliveryId}/replay

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

webhookIdstring <uuid>required

Id of the webhook, as returned by POST /webhooks or GET /webhooks.

deliveryIdstring <uuid>required

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

deliveryobjectrequired

One attempt series at delivering one event to one webhook.

delivery.idstring <uuid>required

The same id the delivery carries in its x-yeetpost-delivery header.

delivery.eventIdstring <uuid>required

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.

delivery.eventstringrequired

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

delivery.statusstringrequired

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

delivery.attemptsintegerrequired

How many times it has been sent. Up to 7, and back to 0 after a replay.

delivery.createdAtstring <date-time>required

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

delivery.nextAttemptAtstring <date-time>required

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

delivery.deliveredAtstring <date-time> | nullrequired

Same format as Timestamp, or null.

delivery.deadAtstring <date-time> | nullrequired

Same format as Timestamp, or null.

delivery.lastStatusCodeinteger | nullrequired

The status your receiver answered the last attempt with, or null when the request never got a response.

delivery.lastErrorstring | nullrequired

Our own one-line summary of why the last attempt did not land, or null. Never your receiver's response body.

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

400invalid_request

Malformed ids, or a delivery that is already queued for another attempt.

401unauthorized

Missing or invalid API key.

404not_found

No webhook of yours with that id, or no delivery of that webhook with that id.

429too many requests

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

500internal_server_error

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/webhooks/{webhookId}/test

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

webhookIdstring <uuid>required

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

deliveryobjectrequired

One attempt series at delivering one event to one webhook.

delivery.idstring <uuid>required

The same id the delivery carries in its x-yeetpost-delivery header.

delivery.eventIdstring <uuid>required

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.

delivery.eventstringrequired

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

delivery.statusstringrequired

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

delivery.attemptsintegerrequired

How many times it has been sent. Up to 7, and back to 0 after a replay.

delivery.createdAtstring <date-time>required

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

delivery.nextAttemptAtstring <date-time>required

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

delivery.deliveredAtstring <date-time> | nullrequired

Same format as Timestamp, or null.

delivery.deadAtstring <date-time> | nullrequired

Same format as Timestamp, or null.

delivery.lastStatusCodeinteger | nullrequired

The status your receiver answered the last attempt with, or null when the request never got a response.

delivery.lastErrorstring | nullrequired

Our own one-line summary of why the last attempt did not land, or null. Never your receiver's response body.

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

400invalid_request

Malformed webhook id.

401unauthorized

Missing or invalid API key.

404not_found

No webhook of yours with that id.

429too many requests

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

500internal_server_error

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/health

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

statusstringrequired

ok | degraded

checksobjectrequired
checks.databaseobjectrequired
checks.database.okbooleanrequired
checks.database.latencyMsintegerrequired

How long the health query took.

checks.scheduledPostsobjectrequired

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.

checks.scheduledPosts.okbooleanrequired
checks.scheduledPosts.lastRunAtstring <date-time> | nullrequired

When the loop last ran, whether or not that iteration finished.

checks.scheduledPosts.lastOkAtstring <date-time> | nullrequired

When the loop last finished an iteration.

checks.webhookDeliveriesobjectrequired

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.

checks.webhookDeliveries.okbooleanrequired
checks.webhookDeliveries.lastRunAtstring <date-time> | nullrequired

When the loop last ran, whether or not that iteration finished.

checks.webhookDeliveries.lastOkAtstring <date-time> | nullrequired

When the loop last finished an iteration.

checks.xUsageSweepobjectrequired

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.

checks.xUsageSweep.okbooleanrequired
checks.xUsageSweep.lastRunAtstring <date-time> | nullrequired

When the loop last ran, whether or not that iteration finished.

checks.xUsageSweep.lastOkAtstring <date-time> | nullrequired

When the loop last finished an iteration.

versionstringrequired

The API version that answered.

timestring <date-time>required

When the health was measured, ISO 8601 UTC.

response
{
  "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/health/history

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

daysinteger, 1 to 90, default 90

How many days back to aggregate, 1 to 90.

response 200

One aggregate per UTC day, oldest first.

response body

daysobject[]required
days[].daystringrequired

The UTC day, YYYY-MM-DD.

days[].uptimePercentnumber | nullrequired

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.

days[].samplesintegerrequired

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.

days[].degradedMinutesintegerrequired

Minutes of that day that were not ok, the ones with no sample included. Zero for an unmeasured day.

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

400invalid_request

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/stats

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

postsThisWeekintegerrequired

Posts that reached a platform in the last 7 days.

postsAllTimeintegerrequired

Posts that reached a platform, ever.

liveConnectionsintegerrequired

Connections that can be posted to right now, sandbox ones left out.

platformsLivestring[]required

The platforms a post can go out to today.

timestring <date-time>required

When the counts were taken, ISO 8601 UTC.

response
{
  "postsThisWeek": 1284,
  "postsAllTime": 41903,
  "liveConnections": 372,
  "platformsLive": [
    "linkedin",
    "x",
    "bluesky",
    "discord",
    "telegram",
    "mastodon"
  ],
  "time": "2026-08-30T11:05:00.512Z"
}
GET/openapi.json

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.

POSTpost.sent

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

x-yeetpost-eventstringrequired

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

x-yeetpost-event-idstring <uuid>required

Id of the event, the same value as eventId in the body. Stable across every retry and every replay of one event, so it is the key to deduplicate on.

x-yeetpost-deliverystring <uuid>required

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

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

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

x-yeetpost-modestringrequired

test when the post behind the delivery was made with a test key, live otherwise. Key your receiver off this to keep sandbox traffic out of its real handling.

body

eventIdstring <uuid>required

Id of this event, the same value as the x-yeetpost-event-id header. Every retry and every replay of one event carries the same id, so a receiver that remembers what it has handled cannot handle it twice.

eventstringrequired

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

timestampstring <date-time>required

When the event was queued.

testboolean

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

true

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

sent | failed

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

Same format as Timestamp, or null.

post.createdAtstring <date-time>required

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

post.mediaCountintegerrequired

How many images the post carried. The ids are left out on purpose: read the bytes back with GET /media/{mediaId} if you need them.

post.threadCountintegerrequired

How many replies went out under the head post.

post.hasFirstCommentbooleanrequired

Whether the first comment landed.

post.errorobject | nullrequired

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

post.error.codestringrequired

Same vocabulary as the top-level error field.

post.error.messagestringrequired
post.tagsstring[], at most 10 itemsrequired

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

post.profileIdstring <uuid> | nullrequired

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

delivery
{
  "eventId": "8d3f2b7c-4e1a-4c6d-9b0f-1a2b3c4d5e6f",
  "event": "post.sent",
  "timestamp": "2026-08-26T09:24:12.004Z",
  "post": {
    "id": "6f1b0c8a-1f3d-4d3e-9a2b-2f5a1c9e7d10",
    "connectionSlug": "linkedin",
    "text": "shipped the new api docs today",
    "status": "sent",
    "url": "https://www.linkedin.com/feed/update/urn:li:share:1234567890",
    "scheduledFor": null,
    "createdAt": "2026-08-26T09:24:11.412Z",
    "error": null
  }
}

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.

POSTpost.failed

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

x-yeetpost-eventstringrequired

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

x-yeetpost-event-idstring <uuid>required

Id of the event, the same value as eventId in the body. Stable across every retry and every replay of one event, so it is the key to deduplicate on.

x-yeetpost-deliverystring <uuid>required

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

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

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

x-yeetpost-modestringrequired

test when the post behind the delivery was made with a test key, live otherwise. Key your receiver off this to keep sandbox traffic out of its real handling.

body

eventIdstring <uuid>required

Id of this event, the same value as the x-yeetpost-event-id header. Every retry and every replay of one event carries the same id, so a receiver that remembers what it has handled cannot handle it twice.

eventstringrequired

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

timestampstring <date-time>required

When the event was queued.

testboolean

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

true

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

sent | failed

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

Same format as Timestamp, or null.

post.createdAtstring <date-time>required

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

post.mediaCountintegerrequired

How many images the post carried. The ids are left out on purpose: read the bytes back with GET /media/{mediaId} if you need them.

post.threadCountintegerrequired

How many replies went out under the head post.

post.hasFirstCommentbooleanrequired

Whether the first comment landed.

post.errorobject | nullrequired

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

post.error.codestringrequired

Same vocabulary as the top-level error field.

post.error.messagestringrequired
post.tagsstring[], at most 10 itemsrequired

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

post.profileIdstring <uuid> | nullrequired

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

delivery
{
  "eventId": "1c4e5f6a-7b8c-4d9e-8f0a-1b2c3d4e5f60",
  "event": "post.failed",
  "timestamp": "2026-08-26T09:24:12.004Z",
  "post": {
    "id": "0b7e4a52-9f0c-4a1c-8b3a-6d2f5b1c0e44",
    "connectionSlug": "x",
    "text": "shipped the new api docs today",
    "status": "failed",
    "url": null,
    "scheduledFor": null,
    "createdAt": "2026-08-26T09:24:11.412Z",
    "error": {
      "code": "platform_rejected",
      "message": "X: duplicate post was detected"
    }
  }
}

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.

POSTpost.deleted

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

x-yeetpost-eventstringrequired

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

x-yeetpost-event-idstring <uuid>required

Id of the event, the same value as eventId in the body. Stable across every retry and every replay of one event, so it is the key to deduplicate on.

x-yeetpost-deliverystring <uuid>required

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

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

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

x-yeetpost-modestringrequired

test when the post behind the delivery was made with a test key, live otherwise. Key your receiver off this to keep sandbox traffic out of its real handling.

body

eventIdstring <uuid>required

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

eventstringrequired

post.deleted

timestampstring <date-time>required

When the event was queued.

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

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

post.deletedAtstring <date-time>required

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

post.profileIdstring <uuid> | nullrequired

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

delivery
{
  "eventId": "2b7c8d3f-4e1a-4c6d-9b0f-1a2b3c4d5e6f",
  "event": "post.deleted",
  "timestamp": "2026-08-29T11:02:44.204Z",
  "post": {
    "id": "6f1b0c8a-1f3d-4d3e-9a2b-2f5a1c9e7d10",
    "connectionSlug": "linkedin",
    "platformId": "linkedin",
    "deletedAt": "2026-08-29T11:02:44.108Z"
  }
}

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.

POSTpost.metrics

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

x-yeetpost-eventstringrequired

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

x-yeetpost-event-idstring <uuid>required

Id of the event, the same value as eventId in the body. Stable across every retry and every replay of one event, so it is the key to deduplicate on.

x-yeetpost-deliverystring <uuid>required

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

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

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

x-yeetpost-modestringrequired

test when the post behind the delivery was made with a test key, live otherwise. Key your receiver off this to keep sandbox traffic out of its real handling.

body

eventIdstring <uuid>required

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

eventstringrequired

post.metrics

timestampstring <date-time>required

When the event was queued.

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

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

post.urlstring | nullrequired

The post's permalink.

post.profileIdstring <uuid> | nullrequired

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

metricsobjectrequired

One read of a post's numbers, as the platform reported them at that moment. Every counter is a running total, so the newest read is the post's numbers now.

metrics.fetchedAtstring <date-time>required

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

metrics.impressionsinteger | nullrequired

How many times the post was shown. X and LinkedIn pages report it; Bluesky, Mastodon and personal LinkedIn profiles do not. Null means the platform does not report this number, which is not the same as zero.

metrics.likesinteger | nullrequired

Likes, favourites or reactions, whatever the platform calls them. Null means the platform does not report this number, which is not the same as zero.

metrics.repostsinteger | nullrequired

Reposts, retweets or boosts. On Bluesky quotes are counted here too. LinkedIn does not report it. Null means the platform does not report this number, which is not the same as zero.

metrics.repliesinteger | nullrequired

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

metrics.clicksinteger | nullrequired

Link clicks. LinkedIn pages report it; nobody else does. Null means the platform does not report this number, which is not the same as zero.

delivery
{
  "eventId": "8d3f2b7c-4e1a-4c6d-9b0f-1a2b3c4d5e6f",
  "event": "post.metrics",
  "timestamp": "2026-08-30T10:00:00.120Z",
  "post": {
    "id": "6f1b0c8a-1f3d-4d3e-9a2b-2f5a1c9e7d10",
    "connectionSlug": "bluesky",
    "profileId": null,
    "platformId": "bluesky",
    "url": "https://bsky.app/profile/alex.bsky.social/post/3l2k"
  },
  "metrics": {
    "fetchedAt": "2026-08-30T10:00:00.000Z",
    "impressions": null,
    "likes": 12,
    "reposts": 3,
    "replies": 1,
    "clicks": null
  }
}

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.

POSTconnection.created

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

x-yeetpost-eventstringrequired

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

x-yeetpost-event-idstring <uuid>required

Id of the event, the same value as eventId in the body. Stable across every retry and every replay of one event, so it is the key to deduplicate on.

x-yeetpost-deliverystring <uuid>required

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

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

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

x-yeetpost-modestringrequired

test when the post behind the delivery was made with a test key, live otherwise. Key your receiver off this to keep sandbox traffic out of its real handling.

body

eventIdstring <uuid>required

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

eventstringrequired

connection.created | connection.removed

timestampstring <date-time>required

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

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

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

connection.connectSessionIdstring <uuid> | nullrequired

The connect session the account came in through, null when the connection was made in the console. Lets you file the connection against the customer you made the link for without waiting for their browser to come back.

connection.platformIdstringrequired
connection.slugstringrequired
delivery
{
  "eventId": "1c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
  "event": "connection.created",
  "timestamp": "2026-08-29T09:24:12.004Z",
  "connection": {
    "id": "3f2a1b4c-5d6e-4f70-8a9b-0c1d2e3f4a5b",
    "profileId": "b2d1f0a3-5c6e-4f70-8a9b-0c1d2e3f4a5b",
    "connectSessionId": "7f3c9a10-2b4d-4e88-9f01-3a5b7c9d1e2f",
    "platformId": "linkedin",
    "slug": "linkedin"
  }
}

response 2XX

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

POSTconnection.removed

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

x-yeetpost-eventstringrequired

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

x-yeetpost-event-idstring <uuid>required

Id of the event, the same value as eventId in the body. Stable across every retry and every replay of one event, so it is the key to deduplicate on.

x-yeetpost-deliverystring <uuid>required

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

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

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

x-yeetpost-modestringrequired

test when the post behind the delivery was made with a test key, live otherwise. Key your receiver off this to keep sandbox traffic out of its real handling.

body

eventIdstring <uuid>required

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

eventstringrequired

connection.created | connection.removed

timestampstring <date-time>required

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

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

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

connection.connectSessionIdstring <uuid> | nullrequired

The connect session the account came in through, null when the connection was made in the console. Lets you file the connection against the customer you made the link for without waiting for their browser to come back.

connection.platformIdstringrequired
connection.slugstringrequired
delivery
{
  "eventId": "1c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
  "event": "connection.removed",
  "timestamp": "2026-08-29T09:24:12.004Z",
  "connection": {
    "id": "3f2a1b4c-5d6e-4f70-8a9b-0c1d2e3f4a5b",
    "profileId": "b2d1f0a3-5c6e-4f70-8a9b-0c1d2e3f4a5b",
    "connectSessionId": null,
    "platformId": "linkedin",
    "slug": "linkedin"
  }
}

response 2XX

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

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