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.

FieldMeaning
platformyoutube, tiktok, facebook, instagram, …
handleThe public handle, e.g. @mychannel
tokenStatushealthy = ready to publish. Anything else means the channel must be reconnected in the web app.
supportsCommentsWhether the platform accepts automated comments
followersCountFollowers 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 endpointmoduleKey to estimate with
POST /videosnews
POST /knowledge/renderknowledge
POST /storytelling/renderstorytelling
POST /comics/{id}/rendercomic
POST /video-talk/renderthe moduleKey you send
POST /ai-videothe moduleKey you send (image-to-video when omitted)
POST /ad-video/renderthe template's variant (product-review, tvc, ugc)
jobs ai-presenter-render / video-clone-renderai-presenter / video-clone, durationSec = scenes × 10

When the real charge can differ

  • POST /ai-video and POST /ad-video/render without serverTier pick the cheapest server taking work, and the duration may be rounded up to that server's clip length. Pass serverTier when 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 (scope credits:read) — every balance change with the balance right after it. type is deposited, spent, refunded or revoked; a not_charged row records a failed video that was not billed.
  • /usage-logs (scope usage: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 (scope usage: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 given timeZone (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. 1

    Request an upload ticket

    POST /media/upload-ticket
    { "contentType": "video/mp4" }

    Returns url, fields, key, expiresAt and maxBytes.

  2. 2

    Send the file to that address

    A multipart POST to url, carrying every entry of fields verbatim and before the file part. Wrong order and storage rejects it.

  3. 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 one connectionIds. The account needs the Upload video plan; without it the call returns 403 upload_video_not_entitled.
  • {id} is a video file in the library that is already ready. Leave scheduleAt empty to publish now; a scheduled time must be in the future and at most 30 days ahead (schedule_in_past, schedule_too_far).
  • coverMediaId is 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 (default true), youtubeConfig, facebookConfig, comment.
  • Requires the posts:write scope and an Idempotency-Key header: 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
  • overview returns 30-day views, engagement rate, post count, new followers, a daily series, and a per-channel breakdown.
  • videos returns a leaderboard. sort accepts views, likes or recent.
  • channelIds accepts 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}.