API reference
Publish from your own script, bot or agent
Everything the composer does is available over HTTP. Create a key in your dashboard, and one call publishes to as many of your connected accounts as you like.
Base URL
Every path below is relative to your Uploader host. All traffic is HTTPS.
https://uploader.stapilo.com
Authentication
Create a key under API keys in your dashboard. It is shown once, when it is created, and stored only as a hash — if you lose it, revoke it and make another. A key acts as you: anything done with it counts as done by you, so treat it like a password.
Send it in whichever of these three suits your client:
Authorization: Bearer <your-key>
x-api-key: <your-key>
?key=<your-key>
The query parameter is there for tools that cannot set a header. Prefer a header where you can — URLs end up in logs.
List your accounts
GET /accounts
Returns the accounts you have connected, with the ids you pass
to /publish.
curl https://uploader.stapilo.com/accounts \
-H "Authorization: Bearer $UPLOADER_KEY"
{
"accounts": [
{
"id": "instagram:17841400000000000",
"platform": "instagram",
"name": "@yourhandle",
"connectedAt": "2026-08-01T09:14:22.000Z"
}
]
}
Publish a post
POST /publish
Send text, media, or both. With no accounts field
the post goes to every account you have connected.
curl https://uploader.stapilo.com/publish \
-H "Authorization: Bearer $UPLOADER_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "Shipped something today.",
"image": "https://example.com/photo.jpg",
"accounts": ["instagram:17841400000000000", "x:1466240000000000000"]
}'
Body fields
| Field | Type | Meaning |
|---|---|---|
accounts | string[] | Which accounts to publish to. Omit it and every connected account is used. |
text | string | The text of the post. |
image | string | string[] | Public URL of a photo, or several. Ignored by networks that take no photo. |
video | string | Public URL of a video. |
reel | boolean | Publish the video as a Reel where the network has them. |
story | boolean | Publish as a Story where the network has them. |
youtubePrivacy | string | YouTube visibility: public, unlisted or private. Anything else is treated as unlisted. |
youtubeTitle | string | Title of the YouTube upload, up to 100 characters. Left out, the first line of the text is used. |
youtubeDescription | string | Description of the YouTube upload. Left out, the text is used. |
Give at least text or one piece of media, or the call
is rejected. Media has to be reachable from the public internet — several networks
fetch it themselves rather than accepting an upload.
What comes back
One result per account, in the order they were published, each naming the account and whether it worked.
{
"results": [
{ "accountId": "instagram:17841400000000000", "platform": "instagram", "ok": true,
"externalPostId": "17912...", "permalink": "https://www.instagram.com/p/..." },
{ "accountId": "x:1466240000000000000", "platform": "x", "ok": false,
"error": "X media upload is being added next — text posts work now." }
]
}
Status codes
200— every account published.207— some accounts published and some did not. Read the results; the ones that failed carry a reason.400— the post was empty, or none of the account ids belong to you.401— the key is missing or not valid.
A per-account failure is a 207, not an error: one network being
down is not a reason to fail the other ten.
Worth knowing
- Account ids belong to you. Passing one that does not is not an error — it is
simply dropped, and if nothing is left you get a
400. - Networks apply their own rate limits and their own rules about what may be posted. We pass their refusal back to you rather than hiding it.
- What each network accepts is listed in the documentation. Sending a video to a network that takes none fails only for that account.