Publishing
Posting
Create, list, check and cancel posts.
A post is some text (and optionally media) sent to one or more connected accounts. Each account gets its own target, with its own status, so one slow or failing platform never holds up the others.
Create a post
POST /api/v1/posts
-
accountsarray of strings · required - The IDs of the accounts to post to, from GET /api/v1/accounts. Between 1 and 50, no duplicates.
-
textstring - The post text, up to 100,000 characters here; each platform’s own limit is checked too.
-
mediaarray of strings - Up to 20 media IDs from POST /api/v1/media.
-
scheduled_atstring - An ISO 8601 time to publish at, up to 1 year ahead. Leave it out to publish now.
-
overridesobject - Per platform: a different text, plus that platform’s options. Keyed by platform, e.g. {"bluesky": {"text": "…"}}.
curl https://socialhelper.app/api/v1/posts \
-H "Authorization: Bearer $SOCIALHELPER_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"text": "We just shipped dark mode. Try it tonight 🌙",
"accounts": [
"ACCOUNT_ID_1",
"ACCOUNT_ID_2"
]
}'
const response = await fetch("https://socialhelper.app/api/v1/posts", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SOCIALHELPER_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
"text": "We just shipped dark mode. Try it tonight 🌙",
"accounts": [
"ACCOUNT_ID_1",
"ACCOUNT_ID_2"
]
}),
});
const { data: post } = await response.json();
console.log(post.status, post.targets);
import os, uuid, requests
response = requests.post(
"https://socialhelper.app/api/v1/posts",
headers={
"Authorization": f"Bearer {os.environ['SOCIALHELPER_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"text": "We just shipped dark mode. Try it tonight 🌙",
"accounts": ["ACCOUNT_ID_1", "ACCOUNT_ID_2"],
},
)
post = response.json()["data"]
print(post["status"], post["targets"])
A new post returns 201. Repeating the request with the same Idempotency-Key returns the original post with 200.
Every target is checked first
Before anything is created, the content is checked against every target’s platform: text length counted the platform’s way, number and type of images and video, file sizes, required options. If anything doesn’t fit, nothing is posted and you get a 422 listing every problem, keyed by the account’s position:
{
"message": "Jane Doe (Bluesky): …",
"errors": {
"accounts.0": [
"Jane Doe (Bluesky): …"
]
}
}
Accounts that can’t receive posts (data accounts) and accounts that need reconnecting are rejected the same way. Each platform’s limits are on its platform page and in GET /api/v1/platforms.
A different text for one platform
Give a platform its own text, for example a shorter version for a platform with a small limit:
{
"text": "We just shipped dark mode. Try it tonight 🌙",
"accounts": [
"ACCOUNT_ID_1",
"ACCOUNT_ID_2"
],
"overrides": {
"bluesky": {
"text": "Dark mode just landed 🌙"
}
}
}
Platforms with extra options (such as a YouTube video’s title) take them in the same place, for example overrides.youtube.title. Options for each platform are listed on its platform page.
Statuses
Each target moves through these statuses:
-
scheduledtarget - Waiting for its scheduled time, or for a retry (next_attempt_at says when).
-
queuedtarget - Due and waiting for a worker.
-
publishingtarget - Being sent to the platform.
-
confirmingtarget - Sent; waiting for the platform to confirm it went live.
-
publishedtarget - Live. url links to it and external_id is the platform’s ID.
-
failedtarget - Didn’t go out; error says why.
-
cancelledtarget - Cancelled before it started.
The post’s own status sums up its targets: scheduled, publishing, published, partially_published, failed or cancelled.
Retries never post twice
If a platform is busy or unreachable before anything was sent, the target goes back to scheduled and is retried later. If a platform’s answer is unclear, SocialHelper checks the platform for the post before trying again where the platform allows it, and otherwise marks the target failed rather than risk a duplicate.
List and fetch posts
GET /api/v1/posts lists posts, newest first, 25 per page (?page=2 for more). Filter with ?status= and a post status. GET /api/v1/posts/{id} returns one post with its targets.
curl https://socialhelper.app/api/v1/posts?status=scheduled \
-H "Authorization: Bearer $SOCIALHELPER_API_KEY"
Cancel a post
DELETE /api/v1/posts/{id} cancels every target that hasn’t started publishing and returns the post. Targets already publishing or live aren’t touched, and posts already on a platform aren’t removed from it. If nothing could be cancelled, you get a 422.