Last updated 4 September 2026
API
Everything ManyHandles does for you, a robot can do too — with a key from Settings. Plain JSON, one workspace per key, the same rules your own clicks follow.
Keys and calls
Make a key under Settings → API. It is shown once. Send it on every request; the base is the host you sign in on, plus /api/v1.
BASE=https://manyhandles.com/api/v1
KEY=mh_live_…
curl "$BASE/me" -H "Authorization: Bearer $KEY"A key acts as the whole workspace: every account it holds, every post, every comment. Revoking a key in Settings stops it at once. Ids are UUIDs, times are ISO 8601 (UTC).
Errors
Every failure is one shape, with a code you can branch on and a sentence you can show:
{ "error": { "code": "not_cancellable", "message": "This post is published — only a scheduled post can be cancelled." } }401 unauthorized— no key, an unknown key, or a revoked one.400 invalid_request— the body or query is wrong; the message names the field.404 not_found— no such post or comment in this workspace.409 not_cancellable/409 not_repliable— the thing exists but is past that action.413 too_large/415 unsupported_media— media over 300 MB, or not mp4, mov, webm, jpeg, png or webp.422 unprocessable— the post could not be planned (daily cap, window too short, no platform fits the piece, TikTok settings missing). Nothing was scheduled.422 fetch_failed— a media URL could not be downloaded in 30 seconds.502 platform_error— the platform refused; the message is its reason.500 internal— our fault. Try again.
GET/me
The workspace this key opens, and its accounts.
curl "$BASE/me" -H "Authorization: Bearer $KEY"
{ "workspace": { "id": "…", "name": "Sep's workspace" },
"accounts": [ { "id": "…", "platform": "instagram", "handle": "shop.nl",
"status": "active", "accessLevel": "inbox" } ] }status is one of active, needs_reauth, paused, disconnected; only active accounts take posts. accessLevel is post, inbox or full.
GET/accounts
The same account list, on its own.
curl "$BASE/accounts" -H "Authorization: Bearer $KEY"POST/media
Add a photo or video to the library and get an id to post with. Either upload the file, or give a public URL and we download it (300 MB, 30 seconds). Send width, height and, for videos, durationMs when you know them — they let us check each platform's rules before posting.
curl "$BASE/media" -H "Authorization: Bearer $KEY" \
-F file=@clip.mp4 -F poster=@clip.jpg -F width=1080 -F height=1920 -F durationMs=14200
curl "$BASE/media" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "url": "https://cdn.example.com/clip.mp4", "width": 1080, "height": 1920, "durationMs": 14200 }'
201 { "id": "…", "storageKey": "3f9c….mp4", "mime": "video/mp4", "width": 1080, "height": 1920 }poster is an optional JPEG still of a video's first frame — it becomes the thumbnail. Without measurements, width and height come back null. Send photos as JPEG: it is the one format every platform takes (Instagram refuses PNG). The composer converts for you; the API stores what you send.
POST/posts
Make a post and spread it over the accounts you name — the same path as the Schedule button. Send a caption, media ids, or both. Answers 201 with one post per account.
curl "$BASE/posts" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "caption": "New colour, same bottle.",
"hashtags": ["skincare", "newin"],
"mediaIds": ["…"],
"accountIds": ["…", "…"],
"when": "now" }'
201 { "pieceId": "…",
"posts": [ { "id": "…", "accountId": "…", "scheduledAt": "2026-09-04T10:12:00.000Z" } ] }when is "now" (the first post at least 10 minutes out, the rest spread over the next 4 hours — the composer's default), one ISO time (every post at that instant; works for one account), or { "start", "end" } — a window we stagger the fleet across, at least 15 minutes apart. Omitted means "now".
options carries per-platform settings, one slice per platform; TikTok requires its slice (privacyLevel and the toggles) before it will take a post:
"options": { "tiktok": { "privacyLevel": "SELF_ONLY", "disableComment": false, "disableDuet": false,
"disableStitch": false, "brandContentToggle": false, "brandOrganicToggle": false,
"isAigc": false },
"threads": { "replyControl": "everyone" },
"instagram": { "postType": "reel" },
"youtube": { "privacy": "unlisted" } }An account whose platform cannot take the piece (a photo on a video-only platform) sits it out; a daily cap or a window too short refuses the whole request with 422 unprocessable and schedules nothing.
GET/posts
Your posts, newest plan first. status narrows to scheduled, queued, publishing, published or failed; limit is 50 by default, 200 at most.
curl "$BASE/posts?status=failed&limit=20" -H "Authorization: Bearer $KEY"
[ { "id": "…", "accountId": "…", "pieceId": "…", "status": "failed",
"scheduledAt": "2026-09-04T10:12:00.000Z", "publishedAt": null,
"platformPostId": null, "platformUrl": null,
"error": { "code": "connector/auth-expired", "message": "…" } } ]GET/posts/:id
One post, same shape as above.
curl "$BASE/posts/$ID" -H "Authorization: Bearer $KEY"DELETE/posts/:id
Cancel a post that has not left yet. Anything already queued, out or failed answers 409 not_cancellable. Returns the post, now canceled.
curl -X DELETE "$BASE/posts/$ID" -H "Authorization: Bearer $KEY"GET/results
What every published post earned — the latest reading per post, newest first. since keeps posts published from that time on; accountId keeps one account. A number the platform did not give us is null, never 0.
curl "$BASE/results?since=2026-09-01T00:00:00Z" -H "Authorization: Bearer $KEY"
[ { "postId": "…", "accountId": "…", "handle": "shop.nl", "platform": "instagram",
"publishedAt": "2026-09-02T17:40:11.000Z", "permalink": "https://www.instagram.com/reel/…",
"views": 12400, "likes": 310, "comments": 12, "shares": 4, "saves": 27, "reach": null } ]GET/inbox
Comments on your posts, newest first. status is new (needs an answer), replied or archived.
curl "$BASE/inbox?status=new" -H "Authorization: Bearer $KEY"
[ { "id": "…", "accountId": "…", "postId": "…",
"author": { "platformId": "1784…", "username": "lisa.k" },
"body": "Does it ship to Belgium?", "status": "new",
"postedAt": "2026-09-03T08:02:51.000Z" } ]POST/inbox/:id/reply
Answer a comment publicly, under it — the Inbox's Reply. The comment is marked replied only after the platform accepts.
curl "$BASE/inbox/$ID/reply" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "message": "Yes — 2 to 3 days." }'
{ "ok": true }