Channels, media and stats
The read-only endpoints every other flow depends on, plus media upload and publishing a finished video as is.
Connected channels
GET /channels?page=1&pageSize=25
This is where you get the id values that go into connectionIds everywhere else.
| Field | Meaning |
|---|---|
platform | youtube, tiktok, facebook, instagram, … |
handle | The public handle, e.g. @mychannel |
tokenStatus | healthy = ready to publish. Anything else means the channel must be reconnected in the web app. |
supportsComments | Whether the platform accepts automated comments |
followersCount | Followers at the last refresh |
Connecting a new channel happens in the web app
It goes through the platform's own consent screen. The API only lists and uses channels that are already connected.
Credit balance
GET /credits
{ "available": 1250, "held": 36 }available— spendable right now.held— reserved for work in flight. Settled when the work finishes, returned when it fails.
Check this before a batch if you would rather stop early than let individual calls fail on insufficient funds. See How credits are calculated.
Estimate before creating
POST /credits/estimate
{ "moduleKey": "image-to-video", "serverTier": 1, "durationSec": 10 }Reads the price list only — nothing is held or charged (scope credits:read). total is what will be held when creating with exactly these inputs; serverTiers gives each server's price and whether it is taking work. serverTier defaults to 1; durationSec (1–300) only matters for billing = per-second.
| Create endpoint | moduleKey to estimate with |
|---|---|
POST /videos | news |
POST /knowledge/render | knowledge |
POST /storytelling/render | storytelling |
POST /comics/{id}/render | comic |
POST /video-talk/render | the moduleKey you send |
POST /ai-video | the moduleKey you send (image-to-video when omitted) |
POST /ad-video/render | the template's variant (product-review, tvc, ugc) |
jobs ai-presenter-render / video-clone-render | ai-presenter / video-clone, durationSec = scenes × 10 |
When the real charge can differ
POST /ai-videoandPOST /ad-video/renderwithoutserverTierpick the cheapest server taking work, and the duration may be rounded up to that server's clip length. PassserverTierwhen creating for a guaranteed price.- AI avatar and motion control are billed on the duration measured from your file.
- Server status is the general status; the real call also depends on how many images you send.
Credit and call history
GET /credits/transactions?type=spent&from=2026-09-01T00:00:00Z&videoId=… GET /usage-logs?channel=mcp&outcome=error&apiKeyId=… GET /usage-logs/stats?channel=api&timeZone=Asia/Ho_Chi_Minh
/credits/transactions(scopecredits:read) — every balance change with the balance right after it.typeisdeposited,spent,refundedorrevoked; anot_chargedrow records a failed video that was not billed./usage-logs(scopeusage:read) — every API call made with a key and every MCP tool an AI assistant used: operation, HTTP status, error code, the key or app that called, and duration. Kept for 90 days; request contents are never stored./usage-logs/stats(scopeusage:read) — total calls, error rate, average/p50/p95 response time, a time series and the 10 most called operations. Defaults to the last 24 hours, up to 90 days; buckets are minutes, hours or days depending on the span, counted in the giventimeZone(IANA).
A from later than to is rejected with 422 instead of returning an empty list, which would read as "nothing happened".
Media library
Uploads take three steps. The file never passes through the API server — you send it straight to storage, so large files neither consume API bandwidth nor hit request-body limits.
- 1
Request an upload ticket
POST /media/upload-ticket { "contentType": "video/mp4" }Returns
url,fields,key,expiresAtandmaxBytes. - 2
Send the file to that address
A multipart
POSTtourl, carrying every entry offieldsverbatim and before the file part. Wrong order and storage rejects it. - 3
Confirm
POST /media { "key": "…", "originalName": "clip.mp4" }Only after this does the file appear in the library. Skip it and the object is orphaned and eventually swept away.
GET /media?limit=25&startingAfter={id}&folderId={id}
GET /media/{id}
DELETE /media/{id}Media pages by cursor: pass the id of the last item of the previous page as startingAfter. The reasoning is in Shared conventions.
Only videos in the ready state can be published
Uploaded video still goes through processing. playbackStatus says whether it is done — feeding a file that is not yet ready into a video creation fails much later, at render time.
Publish a finished video to channels
A video you already produced elsewhere is published as is — no re-render and no credit charged, same as the Publish existing video page in the app.
POST /media/{id}/publish → 202
Idempotency-Key: …
{
"title": "Title (used for YouTube)",
"caption": "Post description",
"hashtags": ["tips", "howto"],
"connectionIds": ["…"],
"scheduleAt": "2026-10-01T10:00:00+07:00",
"coverMediaId": "…"
}- Required:
title,caption(may be empty),hashtags(may be an empty array) and at least oneconnectionIds. The account needs the Upload video plan; without it the call returns 403upload_video_not_entitled. {id}is a video file in the library that is alreadyready. LeavescheduleAtempty to publish now; a scheduled time must be in the future and at most 30 days ahead (schedule_in_past,schedule_too_far).coverMediaIdis an image in the library used as the cover; leave it empty to use a frame extracted from the video.- Same extra options as
POST /videos/{id}/publish:allowComment,allowContentReuse(defaulttrue),youtubeConfig,facebookConfig,comment. - Requires the
posts:writescope and anIdempotency-Keyheader: every call creates a new post on a real channel, so resending the same key after a network drop returns the original post instead of publishing twice.
The response is a new video, with warnings when a channel has hit the recommended number of posts in 24 hours. Track each channel through GET /videos/{id} (targets[]) — see Videos. To publish several files, call once per file.
Brand templates
GET /templates?page=1&pageSize=25
GET /templates/{id}Take an id here and pass it as templateId when creating a video or configuring a source. The response is deliberately small: name, kind, themeId and bgmEnabled.
The ~50 coordinate and colour fields inside a template are absent: they are how the renderer draws frames, and exposing them would tie your contract to one rendering approach. Edit templates in the web app.
Performance stats
GET /analytics/overview GET /analytics/videos?sort=views&limit=20&channelIds=a,b
overviewreturns 30-day views, engagement rate, post count, new followers, a daily series, and a per-channel breakdown.videosreturns a leaderboard.sortacceptsviews,likesorrecent.channelIdsaccepts both repeated parameters (?channelIds=a&channelIds=b) and a comma-separated list. Omit it to cover every channel.
insightIssue explains why a channel's numbers are incomplete
It returns a code, not prose — codes are comparable, wording is not. null means everything was retrieved.
platformVideoId in the leaderboard is the post id on that platform, not an id in this system — do not feed it to GET /videos/{id}.
