Quick start
- In Scheduler, switch to the business you want to post for and open Accounts.
- Under API keys, create a key and copy it. It's shown once; we only keep a fingerprint of it.
- Schedule a text post to every account in that business:
curl -X POST https://zaarmtech.com/api/v1/posts \
-H "Authorization: Bearer $ZAARM_API_KEY" \
-H "Content-Type: application/json" \
-d '{"caption": "Fresh sourdough, out at 7 every morning", "scheduled_at": "2026-11-20T07:00:00Z"}'That's it. The post shows up in the business's Queue in Scheduler with an API badge, and you can cancel it there or through the API. Posts with a photo or video take one extra call to upload the file first.
Authentication
Send your key in the Authorization header of every request. Keys start with zts_.
Authorization: Bearer zts_Xb3…A key belongs to one business and can only see and post to that business's accounts. Create a separate key for each tool that uses the API, so you can revoke one without breaking the others. Revoking a key stops it working on the very next request; posts it already scheduled stay scheduled.
Requests and limits
- Base URL:
https://zaarmtech.com/api/v1. HTTPS only. - Requests and responses are JSON (
Content-Type: application/json), except media uploads, which send the raw file. - Times are ISO 8601 in UTC, e.g.
2026-11-20T07:00:00Z. Ids are opaque strings. - Rate limits: each business can make 120 requests and 30 uploads an hour. The budget is shared by all its keys and by people using the Scheduler app for that business (an upload counts as a request too; in the app, each file uploaded and each Schedule press count). Your business's own limit is shown under Accounts → API keys. Over the limit you get
429with aRetry-Afterheader in seconds. Need more? Email admin@zaarmtech.com. - The API is meant for servers. Browsers on other sites can't call it, so your key never has to be in a web page.
List accounts
GET/api/v1/accounts
The accounts this key's business can post to right now. Use their ids to post to only some of them.
{
"accounts": [
{ "id": "cm3k1…", "platform": "facebook", "name": "Corner Bakery", "username": null },
{ "id": "cm3k2…", "platform": "instagram", "name": "cornerbakery", "username": "cornerbakery" },
{ "id": "cm3k3…", "platform": "youtube", "name": "Corner Bakery", "username": null }
]
}Accounts whose login needs reconnecting aren't listed until you reconnect them in Scheduler.
Upload media
POST/api/v1/media
Upload one photo or video as the raw request body (not a multipart form). The file type is read from the file itself, so the Content-Type header doesn't matter. Use the returned id in media_ids when you schedule a post.
| Type | Accepted | Max size |
|---|---|---|
| Photo | JPG, PNG, WebP | 25 MB |
| Video | MP4, MOV (vertical 9:16 recommended for Reels and Shorts) | 300 MB |
curl -X POST https://zaarmtech.com/api/v1/media \
-H "Authorization: Bearer $ZAARM_API_KEY" \
--data-binary @loaf.jpg{
"id": "4f9c2a7e1b3d4c5e6f7a8b9c0d1e2f3a.jpg",
"type": "image",
"mime_type": "image/jpeg",
"size_bytes": 482113,
"expires_at": "2026-11-20T07:00:00.000Z"
}An upload you don't use in a post is deleted after 7 days. Uploads belong to the business whose key uploaded them.
Schedule a post
POST/api/v1/posts
Schedules one post. It goes to every account you list, or, if you leave accounts out, to every account in the business that can take it. Each account gets its own post, so one failing never blocks the others.
| Field | Description |
|---|---|
| caption | Text of the post, up to 5,000 characters. For YouTube, the first line becomes the title (with #Shorts added). |
| media_ids | Optional. Ids from POST /media: up to 10 photos, or exactly 1 video. |
| accounts | Optional. Account ids from GET /accounts. Leave out to post to all suitable accounts. |
| scheduled_at | Optional. When to publish, 10 minutes to 30 days from now (29 days for video). Defaults to 15 minutes from now. |
A post needs a caption, media, or both.
{
"caption": "Dumbbell curl: 3 mistakes to fix\nPin your elbows. Form first. #gym",
"media_ids": ["9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d.mp4"],
"accounts": ["cm3k1…", "cm3k3…"],
"scheduled_at": "2026-11-20T09:00:00Z"
}{
"posts": [
{
"id": "cm3p9…",
"account": { "id": "cm3k1…", "platform": "facebook", "name": "Corner Bakery" },
"caption": "Dumbbell curl: 3 mistakes to fix\nPin your elbows. Form first. #gym",
"scheduled_at": "2026-11-20T09:00:00.000Z",
"status": "scheduled",
"error": null,
"permalink": null,
"published_at": null,
"created_at": "2026-11-13T08:12:40.000Z"
},
{
"id": "cm3pa…",
"account": { "id": "cm3k3…", "platform": "youtube", "name": "Corner Bakery" },
"status": "scheduled",
"permalink": "https://youtube.com/shorts/abc123",
"…": "…"
}
]
}Check each post's status. A 201 means the request was valid and every post was created, but a platform can still reject one, for example a photo it finds too small. That post comes back as failed with the platform's reason in error, and the rest are still scheduled. Videos are uploaded to Facebook and YouTube during the request, so allow up to a few minutes before timing out.
List posts
GET/api/v1/posts?limit=50
The business's posts, latest scheduled time first, whether they came from the API or the app. limit is 1–100 (default 50).
{ "posts": [ { "id": "cm3p9…", "status": "published", "permalink": "https://www.facebook.com/…", "…": "…" } ] }Get a post
GET/api/v1/posts/{id}
One post, in the same shape as above. Poll this to see when it's published and get its link.
Cancel a post
DELETE/api/v1/posts/{id}
Cancels a post that hasn't gone out yet, and removes it from Facebook's or YouTube's own queue too.
{ "deleted": true }You get 409 if it's already published or being published at that moment, and 502 if the platform wouldn't drop it (the post then stays in the Queue with the reason, so nothing goes out unnoticed).
Platform rules
| Platform | Takes | How it's published |
|---|---|---|
| Text, up to 10 photos, or 1 video (a Reel) | Handed to Facebook's own scheduler right away. | |
| Up to 10 photos (a carousel) or 1 video (a Reel). Not text alone. | Published by Scheduler at the scheduled minute. | |
| youtube | 1 video only (a Short) | Uploaded as private right away; YouTube makes it public at the scheduled time. |
TikTok accounts can be scheduled in the Scheduler app but not through the API: TikTok requires a person to choose each post's privacy and interaction settings on screen. They don't appear in GET /accounts.
When you leave out accounts, accounts that can't take the post are skipped: a photo post skips YouTube, and a text post skips Instagram and YouTube. When you name an account that can't take it, the request fails with unsupported_for_account and nothing is scheduled.
Post statuses
| status | Meaning |
|---|---|
| scheduled | Held by Facebook or YouTube; it will publish at scheduled_at. |
| queued | Instagram: waiting for its time. |
| scheduling, claimed, publishing | Being handed over or published right now. |
| published | Live. permalink links to it where the platform gives one. |
| failed | The platform rejected it; error says why. |
| missed | Its time passed while publishing was unavailable, so it wasn't posted. |
| needs_review | We couldn't confirm whether it went out. Check the account before retrying. |
The business owner also gets an email when a post ends up failed, missed or needs_review, or when a login needs reconnecting, so problems are noticed even if nobody is polling.
Errors
Every error has the same shape, with a stable code to check and a message for people.
{ "error": { "code": "invalid_time", "message": "scheduled_at must be between 10 minutes and 30 days from now." } }| HTTP | code | When |
|---|---|---|
| 400 | invalid_json, empty_body | The body is missing or isn't a JSON object. |
| 401 | unauthorized | No key, a malformed key, or a revoked key. |
| 404 | not_found, media_not_found, account_not_found | The id doesn't exist in this business. |
| 409 | already_published, publishing | Too late to cancel. |
| 413 | file_too_large | Photo over 25 MB or video over 300 MB. |
| 415 | unsupported_type | Not a JPG, PNG, WebP, MP4 or MOV file. |
| 422 | invalid_caption, invalid_media, empty_post, invalid_time, invalid_accounts, no_accounts, unsupported_for_account | The post can't be scheduled as sent; nothing was created. |
| 429 | rate_limited | The business used its hourly requests or uploads; wait for Retry-After seconds. |
| 502 | cancel_failed | Facebook or YouTube refused to drop the post. |
Full examples
Upload a video, then schedule it to every account in the business:
const BASE = "https://zaarmtech.com/api/v1";
const headers = { Authorization: `Bearer ${process.env.ZAARM_API_KEY}` };
const file = await fs.promises.readFile("curl.mp4");
const media = await (await fetch(`${BASE}/media`, { method: "POST", headers, body: file })).json();
const res = await fetch(`${BASE}/posts`, {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({
caption: "Dumbbell curl: 3 mistakes to fix",
media_ids: [media.id],
scheduled_at: "2026-11-20T09:00:00Z",
}),
});
const { posts, error } = await res.json();
if (error) throw new Error(`${error.code}: ${error.message}`);
for (const p of posts) console.log(p.account.platform, p.status, p.error ?? "");import os, requests
BASE = "https://zaarmtech.com/api/v1"
headers = {"Authorization": f"Bearer {os.environ['ZAARM_API_KEY']}"}
with open("loaf.jpg", "rb") as f:
media = requests.post(f"{BASE}/media", headers=headers, data=f).json()
r = requests.post(f"{BASE}/posts", headers=headers, json={
"caption": "Fresh sourdough, out at 7",
"media_ids": [media["id"]],
"scheduled_at": "2026-11-20T07:00:00Z",
})
r.raise_for_status()
for p in r.json()["posts"]:
print(p["account"]["platform"], p["status"], p["error"])curl -X DELETE https://zaarmtech.com/api/v1/posts/cm3p9… \
-H "Authorization: Bearer $ZAARM_API_KEY"Security
- Treat a key like a password. Keep it in an environment variable or secret store on your server, never in a web page, app bundle or public repository.
- We store only a SHA-256 fingerprint of each key, so we can't show it to you again, and neither can anyone who got hold of our database.
- A key can only reach its own business: its accounts, its uploads and its posts. It can't connect accounts, read anything else, or touch other businesses.
- If a key might have leaked, revoke it under Accounts → API keys and create a new one. Each key shows when it was last used.
- Uploaded files are checked by their content, stored under random names outside any public folder, and deleted 7 days after their post finishes.
See also the privacy policy and terms.