API documentation
Schedule Instagram and TikTok videos and carousels from your own code. The API is included in the Pro and Business plans; every request uses your account's API key and the same monthly quota as the dashboard.
Authentication
API access is available on the Pro and Business plans. Create your key in the dashboard under API. Each account has one key: generating a new one revokes the previous key immediately. Send it as a Bearer token over HTTPS:
Authorization: Bearer pl_live_…
Base URL: https://planudo.com/api/v1. Requests are limited to 120 per minute per key.
Quota
Each post you create (with the API or the dashboard) uses one scheduled post, however many accounts it targets. Quotas reset on the 1st of every month (UTC). Canceling a post before it is published gives it back.
| Plan | Posts / month | Connected accounts | API |
|---|---|---|---|
| Free | 5 | 1 | No |
| Pro | 30 | 5 | Yes |
| Business | 60 | 10 | Yes |
GET/me
{ "email": "you@example.com",
"quota": { "plan": "pro", "limit": 30, "used": 4, "remaining": 26, "accountsLimit": 5, "resetsAt": "2026-12-01T00:00:00.000Z" } }
Connected accounts
Accounts are connected in the dashboard with the official Instagram and TikTok logins. Use their id when you create a post.
GET/accounts
{ "accounts": [
{ "id": "ig_7f3a91bc0d2e4a55", "network": "instagram", "username": "fruitninja.daily", "avatar": "https://…", "lastError": null },
{ "id": "tt_91bc44e0a7d2c310", "network": "tiktok", "username": "Fruit Ninja Daily", "directPost": false, "lastError": null } ] }
Create a post
POST/posts
Send a multipart/form-data body:
| Field | Description |
|---|---|
accounts | Required. JSON array of account ids. |
video | A video file (MP4, MOV or WebM, 3 s to 10 min, 500 MB max). Published as a Reel on Instagram. Send either video or images. |
images | For a carousel: one file per slide, in order (JPEG, PNG or WebP, 30 MB max each). Instagram: 10 max, TikTok: 35 max. |
caption | Full caption, used as-is on Instagram and as the TikTok description. Either caption or title is required. |
title, description, hashtags | Alternative to caption: Planudo builds the caption from these. hashtags is a JSON array or a space-separated string (30 max). The TikTok title is limited to 90 characters. |
scheduledAt | ISO 8601 date. Empty or missing: publish now. Up to one year ahead. |
instagram | Carousels only, optional: a different set of slides for Instagram (for example 4:5 versions). |
tiktok | Optional JSON object, see TikTok options. |
ref | Optional. Your own identifier (200 characters max), to find the post back with GET /posts?ref=…. |
link | Optional. A URL stored with the post (not published). |
curl https://planudo.com/api/v1/posts \
-H "Authorization: Bearer $PLANUDO_KEY" \
-F caption="Fruit Ninja – Season 1, episode 1 🍉 #fruitninja" \
-F accounts='["ig_7f3a91bc0d2e4a55","tt_91bc44e0a7d2c310"]' \
-F scheduledAt=2026-12-01T18:00:00Z \
-F tiktok='{"mode":"inbox"}' \
-F video=@episode-1.mp4
Response 201 with the post. Videos that are not already MP4 H.264/AAC (or larger than 1080 × 1920) are converted. Carousel images are resized for each network (Instagram: between 4:5 and 1.91:1, 1440 px wide max; TikTok: 1080 × 1920 max, no cropping).
Read posts
GET/posts?status=scheduled&ref=…&limit=50
GET/posts/:id
{ "id": "6745…", "status": "partial", "scheduledAt": "2026-12-01T18:00:00.000Z", "source": "api", "ref": "season1-ep1",
"targets": [
{ "network": "instagram", "username": "fruitninja.daily", "status": "published", "url": "https://www.instagram.com/p/…", "error": null },
{ "network": "tiktok", "username": "Fruit Ninja Daily", "status": "failed", "error": "…" } ] }
Post status: scheduled, publishing, done, partial (some accounts failed), failed, canceled. Target status: pending, publishing, published, inbox (sent to the TikTok inbox as a draft), failed, canceled. Filter status also accepts upcoming (scheduled or publishing). Results are sorted by date, newest first; paginate with before (an ISO date).
Cancel and retry
POST/posts/:id/cancel
Only while the post is still scheduled. The scheduled post goes back to your quota.
POST/posts/:id/retry
Publishes again on the accounts that failed only: an account that already received the post never gets it twice. Retrying does not use your quota.
TikTok options
By default ({"mode":"inbox"}) the video or carousel is sent to the creator's TikTok inbox as a draft, to finish in the TikTok app (TikTok does not let apps pre-fill the caption of a video draft). When direct posting is available for an account (directPost: true), you can publish directly. TikTok then requires the creator to choose these settings themselves:
tiktok={"mode":"direct","privacy":"PUBLIC_TO_EVERYONE","allowComments":true,"brandContent":false,"brandOrganic":false,
"allowDuet":true,"allowStitch":true,"aiGenerated":true}
privacy must be one of the options returned for this creator by GET /accounts/:id/tiktok-creator, which also gives the longest video they can post (maxVideoSec). Branded content cannot be private. allowDuet, allowStitch and aiGenerated (label the video as AI-generated) apply to videos; autoAddMusic applies to carousels.
Errors
Errors return JSON: { "error": "message" }.
400 | Invalid request (missing field, bad date, too many images…) |
401 | Missing or invalid API key |
403 | Your plan does not include API access (Free plan) |
402 | Monthly quota reached: upgrade your plan or wait for the 1st of next month |
404 | Post or account not found |
429 | Too many requests |
Node.js example
import fs from 'node:fs/promises';
const form = new FormData();
form.append('caption', 'AI video · Episode 4 ✨');
form.append('accounts', JSON.stringify(['ig_7f3a91bc0d2e4a55']));
form.append('scheduledAt', new Date(Date.now() + 3600e3).toISOString());
form.append('video', new Blob([await fs.readFile('episode-4.mp4')], { type: 'video/mp4' }), 'episode-4.mp4');
const res = await fetch('https://planudo.com/api/v1/posts', {
method: 'POST', headers: { Authorization: `Bearer ${process.env.PLANUDO_KEY}` }, body: form,
});
console.log(res.status, await res.json());