SchedulePost API

Publish, schedule, or queue videos to all your connected platforms from any script or automation.

API Keys

Sign in to generate and manage your API keys.

Authentication

Pass your API key in every request:

x-api-key: sp_your_api_key_here

Base URL

https://schedulepost.fulinlabs.com/api/v1

Endpoints

POST/upload

Upload a media file (video or image) as multipart/form-data or a raw binary body. A Content-Length header bounded by the 512 MB limit is required and is validated before the body is read. JSON presign requests are no longer accepted here — see /api/v2/upload.

Parameters

file(File)— Media file (multipart form field) or raw binary with x-filename

Example

# Multipart form upload
curl -X POST -H "x-api-key: sp_xxx" \
  -F "file=@/path/to/video.mp4" \
  https://schedulepost.fulinlabs.com/api/v1/upload

# Raw binary upload
curl -X POST -H "x-api-key: sp_xxx" \
  -H "x-filename: my-video.mp4" \
  --data-binary @/path/to/video.mp4 \
  https://schedulepost.fulinlabs.com/api/v1/upload

Response

{
  "success": true,
  "videoUrl": "https://pub-xxx.r2.dev/user-id/uuid.mp4",
  "path": "user-id/uuid.mp4",
  "message": "Video uploaded. Use the videoUrl in /publish, /schedule, or /queue."
}
POST/upload

v1 JSON presign requests (Content-Type: application/json) are rejected with 426 Upgrade Required — the v1 contract had no way to bind an exact upload size. Migrate to /api/v2/upload.

Example

curl -X POST -H "x-api-key: sp_xxx" \
  -H "Content-Type: application/json" \
  -d '{"filename":"my-video.mp4","contentType":"video/mp4"}' \
  https://schedulepost.fulinlabs.com/api/v1/upload
# -> 426 Upgrade Required

Response

{
  "error": "UPLOAD_CONTRACT_UPGRADED",
  "message": "JSON presign requests are no longer supported on /api/v1/upload because they cannot bind an exact upload size. Use /api/v2/upload with a bounded contentLength instead.",
  "upgradeTo": "/api/v2/upload"
}
POST/api/v2/upload

Request a presigned R2 upload target for large media. Requires an exact contentLength (bytes), which is bound to the signed PUT so R2 rejects a body that doesn't match.

Parameters

contentLength(number, required)— Exact size of the file in bytes, bounded by the 512 MB limit
filename(string)— Original filename, used only to infer the storage key extension
contentType(string)— MIME type, must be video/* or image/*

Example

curl -X POST -H "x-api-key: sp_xxx" \
  -H "Content-Type: application/json" \
  -d '{"filename":"my-video.mp4","contentType":"video/mp4","contentLength":10485760}' \
  https://schedulepost.fulinlabs.com/api/v2/upload
# PUT the file to the returned uploadUrl with matching Content-Type and
# Content-Length headers, then publish or schedule using the returned videoUrl.

Response

{
  "success": true,
  "uploadUrl": "https://...r2.cloudflarestorage.com/...?X-Amz-Signature=...",
  "videoUrl": "https://pub-xxx.r2.dev/user-id/uuid.mp4",
  "path": "user-id/uuid.mp4",
  "expiresIn": 900,
  "message": "Upload the media with PUT and a matching Content-Length header, then use videoUrl in /publish, /schedule, or /queue."
}
GET/connections

List all connected platform accounts. Use the returned IDs in publish/schedule/queue requests.

Example

curl -H "x-api-key: sp_xxx" \
  https://schedulepost.fulinlabs.com/api/v1/connections

Response

{
  "connections": [
    {
      "id": "uuid-1",
      "platform": "youtube",
      "account_name": "My Channel",
      "account_id": "UC..."
    },
    {
      "id": "uuid-2",
      "platform": "tiktok",
      "account_name": "@myhandle",
      "account_id": "123..."
    }
  ]
}
POST/publish

Publish immediately with text, single media, or carousel media. `videoUrl` is still supported for backward compatibility. TikTok defaults to direct public posting.

Parameters

videoUrl(string)— Legacy single-video URL (backward compatibility)
mediaUrls(string[])— Array of public media URLs. Type is inferred from file extension.
mediaItems({url,mediaType}[])— Preferred media payload for precise control. mediaType: "IMAGE" or "VIDEO". Use 2+ items for carousels.
caption(string)— Post text — you can also use "title", "text", or "description" as aliases
hashtags(string)— Hashtags string (e.g. "#shorts #viral")
connectionIds(string[], required)— Array of connection IDs from /connections
youtubeTitle(string)— Optional YouTube-only title override (max 100 chars)
youtubeDescription(string)— Optional YouTube-only description override
youtubePrivacy(string)— YouTube privacy status: "private" (default), "unlisted", or "public"
tiktokPostMode(string)— TikTok mode: "DIRECT_POST" (default) or "MEDIA_UPLOAD" (draft to TikTok inbox)
tiktokPrivacy(string)— TikTok Direct Post privacy. Defaults to "PUBLIC_TO_EVERYONE". Must match one creator_info option.
tiktokCommercialType(number)— TikTok commercial content type: 0=none, 1=your brand, 2=branded content, 3=both
tiktokDisableComment(boolean)— Disable comments on TikTok post (default: false)
tiktokDisableDuet(boolean)— Disable duet on TikTok post (default: false)
tiktokDisableStitch(boolean)— Disable stitch on TikTok post (default: false)

Example

curl -X POST -H "x-api-key: sp_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "caption": "Check out the new feature demo",
    "mediaItems": [
      { "url": "https://example.com/video-1.mp4", "mediaType": "VIDEO" }
    ],
    "hashtags": "#shorts #viral",
    "connectionIds": ["uuid-1", "uuid-2"],
    "youtubeTitle": "Quarterly feature demo",
    "youtubeDescription": "Long-form YouTube description for the same upload.",
    "youtubePrivacy": "private",
    "tiktokPostMode": "DIRECT_POST",
    "tiktokPrivacy": "PUBLIC_TO_EVERYONE"
  }' \
  https://schedulepost.fulinlabs.com/api/v1/publish

Response

{
  "success": true,
  "allPublished": true,
  "postId": "post-uuid",
  "platforms": [
    {
      "connectionId": "uuid-1",
      "platform": "youtube",
      "success": true,
      "platformPostId": "yt-abc"
    },
    {
      "connectionId": "uuid-2",
      "platform": "tiktok",
      "success": true,
      "platformPostId": "tt-123"
    }
  ]
}
POST/schedule

Schedule text, single media, or carousel media for a specific date/time. `videoUrl` remains supported.

Parameters

videoUrl(string)— Legacy single-video URL
mediaUrls(string[])— Array of public media URLs
mediaItems({url,mediaType}[])— Preferred media payload. mediaType: "IMAGE" | "VIDEO"
caption(string)— Post text — you can also use "title", "text", or "description" as aliases
hashtags(string)— Hashtags string
connectionIds(string[], required)— Array of connection IDs
scheduledFor(string, required)— ISO 8601 datetime (e.g. 2026-03-20T15:00:00Z)
youtubeTitle(string)— Optional YouTube-only title override
youtubeDescription(string)— Optional YouTube-only description override
youtubePrivacy(string)— YouTube privacy status: "private" (default), "unlisted", or "public"
tiktokPostMode(string)— TikTok mode: "DIRECT_POST" (default) or "MEDIA_UPLOAD" (draft to TikTok inbox)
tiktokPrivacy(string)— TikTok Direct Post privacy. Defaults to "PUBLIC_TO_EVERYONE". Must match one creator_info option.
tiktokCommercialType(number)— TikTok commercial content type: 0=none, 1=your brand, 2=branded content, 3=both
tiktokDisableComment(boolean)— Disable comments on TikTok post (default: false)
tiktokDisableDuet(boolean)— Disable duet on TikTok post (default: false)
tiktokDisableStitch(boolean)— Disable stitch on TikTok post (default: false)

Example

curl -X POST -H "x-api-key: sp_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "mediaItems": [
      { "url": "https://example.com/image-a.jpg", "mediaType": "IMAGE" },
      { "url": "https://example.com/image-b.jpg", "mediaType": "IMAGE" }
    ],
    "caption": "Dropping this tomorrow!",
    "connectionIds": ["uuid-1"],
    "scheduledFor": "2026-03-20T15:00:00Z",
    "youtubePrivacy": "private",
    "tiktokPostMode": "DIRECT_POST",
    "tiktokPrivacy": "PUBLIC_TO_EVERYONE"
  }' \
  https://schedulepost.fulinlabs.com/api/v1/schedule

Response

{
  "success": true,
  "postId": "post-uuid",
  "scheduledFor": "2026-03-20T15:00:00.000Z",
  "platforms": ["youtube"],
  "connectionIds": ["uuid-1"]
}
POST/queue

Smart queue — automatically picks the next available slot for text, single media, or carousels. No need to specify a date.

Parameters

videoUrl(string)— Legacy single-video URL
mediaUrls(string[])— Array of public media URLs
mediaItems({url,mediaType}[])— Preferred media payload. mediaType: "IMAGE" | "VIDEO"
caption(string)— Post text — you can also use "title", "text", or "description" as aliases
hashtags(string)— Hashtags string
connectionIds(string[], required)— Array of connection IDs
settings(object)— Override smart schedule settings (postsPerDay: 1|2|3, allowedDays)
youtubeTitle(string)— Optional YouTube-only title override
youtubeDescription(string)— Optional YouTube-only description override
youtubePrivacy(string)— YouTube privacy status: "private" (default), "unlisted", or "public"
tiktokPostMode(string)— TikTok mode: "DIRECT_POST" (default) or "MEDIA_UPLOAD" (draft to TikTok inbox)
tiktokPrivacy(string)— TikTok Direct Post privacy. Defaults to "PUBLIC_TO_EVERYONE". Must match one creator_info option.
tiktokCommercialType(number)— TikTok commercial content type: 0=none, 1=your brand, 2=branded content, 3=both
tiktokDisableComment(boolean)— Disable comments on TikTok post (default: false)
tiktokDisableDuet(boolean)— Disable duet on TikTok post (default: false)
tiktokDisableStitch(boolean)— Disable stitch on TikTok post (default: false)

Example

curl -X POST -H "x-api-key: sp_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "mediaItems": [
      { "url": "https://example.com/photo-1.jpg", "mediaType": "IMAGE" },
      { "url": "https://example.com/photo-2.jpg", "mediaType": "IMAGE" }
    ],
    "caption": "Auto-queued!",
    "connectionIds": ["uuid-1", "uuid-2"],
    "youtubePrivacy": "private",
    "tiktokPostMode": "DIRECT_POST",
    "tiktokPrivacy": "PUBLIC_TO_EVERYONE",
    "settings": {
      "postsPerDay": 2,
      "allowedDays": [1,2,3,4,5]
    }
  }' \
  https://schedulepost.fulinlabs.com/api/v1/queue

Response

{
  "success": true,
  "postId": "post-uuid",
  "scheduledFor": "2026-03-17T14:32:00.000Z",
  "message": "Post added to queue with smart scheduling.",
  "platforms": ["youtube", "tiktok"],
  "connectionIds": ["uuid-1", "uuid-2"],
  "scheduleSettings": {
    "postsPerDay": 2,
    "allowedDays": [1,2,3,4,5]
  }
}
GET/posts

List your posts. Optionally filter by status.

Parameters

status(string)— Query param: "draft", "scheduled", "publishing", "published", "failed", or "partial"

Example

curl -H "x-api-key: sp_xxx" \
  "https://schedulepost.fulinlabs.com/api/v1/posts?status=scheduled"

Response

{
  "posts": [
    {
      "id": "post-uuid",
      "video_url": "https://...",
      "media_items": [{"url":"https://...","mediaType":"IMAGE"}],
      "caption": "...",
      "youtube_title": "My YouTube title",
      "youtube_description": "My YouTube description",
      "youtube_privacy": "private",
      "status": "scheduled",
      "scheduled_for": "2026-03-20T15:00:00Z",
      "platforms": ["youtube"],
      "connection_ids": ["uuid-1"],
      "created_at": "2026-03-31T22:00:00.000Z"
    }
  ]
}
DELETE/posts

Cancel a scheduled post (only works for posts with status 'scheduled').

Parameters

id(string, required)— Post ID to cancel

Example

curl -X DELETE -H "x-api-key: sp_xxx" \
  -H "Content-Type: application/json" \
  -d '{"id": "post-uuid"}' \
  https://schedulepost.fulinlabs.com/api/v1/posts

Response

{ "success": true }
GET/groups

List your account groups — named bundles of platform connection IDs you can target when scheduling or queueing posts. Requires the posts:read scope.

Example

curl -H "x-api-key: sp_xxx" \\
  https://schedulepost.fulinlabs.com/api/v1/groups

Response

{
  "groups": [
    {
      "id": "group-uuid",
      "name": "Main Channel",
      "connection_ids": ["uuid-1", "uuid-2"]
    }
  ]
}

TypeScript Example

const API_KEY = "sp_your_key";
const BASE = "https://schedulepost.fulinlabs.com/api/v1";

// 1. Request a direct upload target (recommended for large videos)
const fs = require("fs");
const upload = await fetch(`${BASE}/upload`, {
  method: "POST",
  headers: { "x-api-key": API_KEY, "Content-Type": "application/json" },
  body: JSON.stringify({ filename: "video.mp4", contentType: "video/mp4" }),
}).then(r => r.json());

await fetch(upload.uploadUrl, {
  method: "PUT",
  headers: { "Content-Type": "video/mp4" },
  body: fs.readFileSync("./video.mp4"),
});

console.log(upload.videoUrl);
// → "https://pub-xxx.r2.dev/user-id/uuid.mp4"

// 2. List connections to find account IDs
const connections = await fetch(`${BASE}/connections`, {
  headers: { "x-api-key": API_KEY },
}).then(r => r.json());

// 3. Publish the uploaded video with YouTube-specific metadata
const result = await fetch(`${BASE}/publish`, {
  method: "POST",
  headers: { "x-api-key": API_KEY, "Content-Type": "application/json" },
  body: JSON.stringify({
    mediaItems: [{ url: upload.videoUrl, mediaType: "VIDEO" }],
    caption: "Feature launch video",
    connectionIds: [connections.connections[0].id],
    youtubeTitle: "Feature launch video",
    youtubeDescription: "Longer YouTube description for the same upload.",
    youtubePrivacy: "private",
  }),
}).then(r => r.json());

console.log(result);
// → { success: true, postId: "...", scheduledFor: "2026-03-17T14:32:00Z", ... }

Error Codes

StatusMeaning
401Missing or invalid API key
400Bad request (missing params, invalid connections, no slots)
404Unknown endpoint
405Method not allowed
500Server error