Configure Videos – Metadata & Scheduling API
Configure video content and metadata via the TokPortal API. Set descriptions, publish dates, sounds, hashtags, and platform-specific options.
Configure Videos
Set content and metadata for videos within a bundle. You can configure a single video by position or update multiple videos in one batch request.
Configure a Single Video
PUT /bundles/:id/videos/:position
Sets or updates the content and metadata for the video at the given position (1-indexed).
Patch Video Metadata
PATCH /bundles/:id/videos/:position
Update lightweight metadata fields (external_ref, name) on a video without re-uploading content or changing its status. Works at any point in the video's lifecycle (including published, accepted, finalized).
Use this endpoint to fix or set your own reference IDs after the fact — no need to re-send the full configuration.
Request Body
All fields are optional. At least one must be provided.
| Field | Type | Description |
|---|---|---|
external_ref | string | null | Your own reference ID for this video. Max 200 characters. Set to null to clear. |
name | string | null | Internal name for the video. Max 200 characters. Set to null to clear. |
Example
curl -X PATCH https://app.tokportal.com/api/ext/bundles/bundle_abc123/videos/1 \
-H "X-API-Key: sk_xxx" \
-H "Content-Type: application/json" \
-d '{
"external_ref": "correct-campaign-id-42"
}'
Response
Returns the full video object (same shape as GET /bundles/:id/videos/:position).
{
"data": {
"id": "vid_xyz",
"position": 1,
"status": "published",
"external_ref": "correct-campaign-id-42",
"name": "Video 1",
"...": "..."
}
}
Error Responses
| Status | Code | Description |
|---|---|---|
400 | VALIDATION_ERROR | No fields provided, or invalid values. |
404 | BUNDLE_NOT_FOUND | Bundle does not exist. |
404 | VIDEO_NOT_FOUND | No video exists at this position. |
403 | BUNDLE_NOT_OWNED | Bundle belongs to another user. |
Configure Multiple Videos (Batch)
PUT /bundles/:id/videos/batch
Updates multiple videos in a single request. The request body must be wrapped in a videos object — it is not a raw array.
{
"videos": [
{ "position": 1, "video_type": "video", "..." : "..." },
{ "position": 2, "video_type": "carousel", "..." : "..." }
]
}
auto_publish — configure and publish in one call
Both PUT /bundles/:id/videos/:position and PUT /bundles/:id/videos/batch accept an optional top-level boolean auto_publish (default false). When true, the API will attempt POST /bundles/:id/publish immediately after a successful video configuration. A common integration mistake is to configure videos and then forget to publish — auto_publish: true removes that step.
For POST /bundles/:id/videos/import-csv (multipart form), pass auto_publish=true as a separate form field alongside file.
Behaviour
- The configuration step is the source of truth: if it succeeds, the response is a success response.
auto_publishfailures never fail the request. - If publish is not currently possible (account not yet configured, missing fields…), the response includes a
blockersarray — same codes asGET /bundles/:id/publish-readiness. - If publish was attempted and succeeded, the bundle moves to
published(orpublished_priority) and a notification is created, identical to a manualPOST /bundles/:id/publish.
auto_publish response object
Every video-configuration response now includes an auto_publish field:
{
"auto_publish": {
"attempted": true,
"published": true,
"bundle_status": "published",
"videos_published": 5
}
}
| Field | Type | Description |
|---|---|---|
attempted | boolean | true only if auto_publish: true was sent and at least one video was successfully configured in this call. |
published | boolean | true if publish succeeded. |
bundle_status | string | New bundle status (only present when published: true). |
videos_published | integer | Number of configured videos included in the publish (only when published: true). |
blockers | array | Present when published: false because publish-readiness checks failed. Each item: { code, message, details? }. See Publish-Readiness for the code list. |
error | object | Present when publish was attempted but failed for a non-readiness reason (e.g., transient RPC failure). Shape: { code, message }. Re-attempt by calling POST /bundles/:id/publish directly. |
Example — configure batch and publish
curl -X PUT https://app.tokportal.com/api/ext/bundles/bnd_abc123/videos/batch \
-H "X-API-Key: sk_xxx" \
-H "Content-Type: application/json" \
-d '{
"auto_publish": true,
"videos": [
{ "position": 1, "video_type": "video", "video_url": "https://...", "description": "...", "target_publish_date": "2026-05-01" }
]
}'
Example — auto_publish skipped because the account is not ready
{
"data": {
"bundle_id": "bnd_abc123",
"configured": 5,
"results": [...],
"auto_publish": {
"attempted": true,
"published": false,
"blockers": [
{
"code": "ACCOUNT_MISSING_FIELDS",
"message": "Account is missing required fields.",
"details": { "missing_fields": ["profile_picture_url"] }
}
]
}
}
}
Field Reference
Common Fields
| Field | Type | Required | Description |
|---|---|---|---|
position | integer | Batch only | 1-indexed video position. Required in batch requests. |
video_type | string | Yes | "video", "carousel", or "story". For Instagram: carousel with instagram_content_type: "reel" = Fixed Photos (creates a video from images), carousel with instagram_content_type: "post" = swipeable Carousel. story is TikTok/Instagram only — see Story Fields. |
description | string | Conditional | Caption or description for the post. Required for video and carousel. Stories have no description (omit it). |
target_publish_date | string | Yes | Desired publish date (YYYY-MM-DD). Min 3 days ahead for new accounts, 1 day for existing. |
video_url | string | Conditional | Video file URL. Required when video_type is "video"; for a story, provide it only if the story is a video. You can pass a public_url from the upload endpoint or any external URL (Google Drive, Dropbox, direct link…) — the API will automatically download and re-upload the file to our storage. |
carousel_images | string[] | Conditional | Array of image storage paths. Required when video_type is "carousel". Use the storage_path from the image upload response. |
tiktok_sound_url | string | Conditional | TikTok sound URL. Required for TikTok carousels, optional for TikTok videos. |
volume_original_sound | integer | No | Volume of the original video sound, from 0 to 200 (percent). 100 = unchanged. Setting this for the first time on a video costs 1 credit (see note below). |
volume_added_sound | integer | No | Volume of the added sound/music track, from 0 to 200 (percent). 100 = unchanged. Setting this for the first time on a video costs 1 credit (see note below). |
editing_instructions | string | No | Free-text instructions for the editor (e.g., "Add logo at end"). |
external_ref | string | No | Your own reference ID for this video (e.g., campaign or CMS identifier). |
profile_picture_url | string | No | Storage path for the account profile picture. Use the storage_path from the image upload response. |
INFO: Sound Volume Control — 1 credit per video Setting
volume_original_soundorvolume_added_soundon a video enables the sound-volume-control feature for that video and costs 1 credit, debited automatically the first time either field is set.
- Both fields accept integers from
0(mute) to200(double volume).100means the sound is untouched.- The credit is charged once per video. Subsequent updates to the same fields on the same video are free.
- If your balance is too low, the API returns
INSUFFICIENT_CREDITSwithfeature: "sound_volume".- Supported on TikTok and Instagram.
Compliance & Repost Options
Optional posting flags. The two disclosure flags are free and simply tell the account manager to toggle the platform's native label when posting. instant_repost_as_story is a paid add-on.
| Field | Type | Required | Description |
|---|---|---|---|
ai_content_disclaimer | boolean | No | AI content notice (free). If true, the manager enables the platform's AI-generated content label when posting. |
disclose_as_ads | boolean | No | Disclose as ad (free). If true, the manager enables branded-content / paid-partnership disclosure (or adds #ad) when posting. |
instant_repost_as_story | boolean | No | Instant repost as story (paid, +1 credit). Right after posting, the manager reposts this video as a story and provides a screenshot proof. Only valid for video_type: "video" on TikTok / Instagram bundles. |
INFO: Instant Repost as Story — 1 credit per video Enabling
instant_repost_as_storycosts 1 credit, debited automatically the first time the flag is turned on for a video (charge-once, no refunds).
- Only allowed on TikTok/Instagram bundles and only for
video_type: "video"(not stories or carousels).- If your balance is too low, the API returns
INSUFFICIENT_CREDITSwithfeature: "instant_repost".- Omit the field to keep the current value — it is never silently cleared.
Instagram-Specific Fields
| Field | Type | Required | Description |
|---|---|---|---|
instagram_content_type | string | Yes (Instagram) | "reel" or "post". Required for Instagram bundles. |
instagram_location | string | No | Location tag for the Instagram post. |
instagram_collaborators | string[] | No | Array of Instagram usernames to tag as collaborators. |
instagram_audio_name | string | No | Name of the audio track for Instagram Reels. |
instagram_add_to_story | boolean | No | If true, the creator also shares the post to their Instagram Story. |
Instagram Content Type Combinations
instagram_content_type | video_type | Result |
|---|---|---|
reel | video | Reel Video |
reel | carousel | Reel Fixed Photos — creates a video from images (not swipeable) |
post | video | Post Video |
post | carousel | Post Carousel — swipeable photo carousel |
Story Fields
A Story is a new content type (TikTok and Instagram only) that counts as one video slot, just like a regular video. A story is a single piece of media — exactly one video or one image (no multi-image) — and has no description.
Because a story can't be shared with a link or verified through the platform API, it is verified by the account manager, who uploads a screenshot as proof. That screenshot is returned on the video object as evidence_screenshot once the story is submitted.
| Field | Type | Required | Description |
|---|---|---|---|
video_type | string | Yes | Set to "story". |
video_url | string | Conditional | The story video. Provide either video_url or story_image_url, never both. Accepts an external URL — auto-downloaded like any other video. |
story_image_url | string | Conditional | The story image. Provide either story_image_url or video_url, never both. Accepts an external URL — auto-downloaded and stored. |
story_repost_url | string | No | Optional. Makes the story repost an existing post from the same platform (a TikTok story must link a tiktok.com post; an Instagram story must link an instagram.com post). Costs +1 credit (see note). A link from any other platform is rejected. |
tiktok_sound_url | string | No | TikTok sound for the story (TikTok stories only). |
instagram_audio_name | string | No | Audio name for the story (Instagram stories only). |
editing_instructions | string | No | Free-text instructions for the editor. |
target_publish_date | string | Yes | Same scheduling rules as regular videos. |
Stories do not accept description, instagram_content_type, instagram_location, instagram_collaborators, carousel_images, or instagram_add_to_story.
INFO: Story Repost Link — 1 credit Setting
story_repost_urlenables the repost feature for that story and costs 1 credit, debited automatically the first time the field is set. If your balance is too low, the API returnsINSUFFICIENT_CREDITSwithfeature: "repost_url". The media (video or image) is always required — the repost link is an additional instruction on top of it.
TikTok Story (image) with a repost link
curl -X PUT https://app.tokportal.com/api/ext/bundles/bundle_abc123/videos/4 \
-H "X-API-Key: sk_xxx" \
-H "Content-Type: application/json" \
-d '{
"video_type": "story",
"target_publish_date": "2026-03-22",
"story_image_url": "https://pub-xxx.r2.dev/images/story-promo.jpg",
"story_repost_url": "https://www.tiktok.com/@brand/video/7300000000000000000"
}'
Response:
{
"data": {
"position": 4,
"video_type": "story",
"status": "configured",
"description": "",
"target_publish_date": "2026-03-22",
"story_image_url": "carousel-images/org_xxx/bundle_abc123/story-promo.jpg",
"story_repost_url": "https://www.tiktok.com/@brand/video/7300000000000000000",
"evidence_screenshot": null
}
}
Instagram Story (video)
curl -X PUT https://app.tokportal.com/api/ext/bundles/bundle_def456/videos/2 \
-H "X-API-Key: sk_xxx" \
-H "Content-Type: application/json" \
-d '{
"video_type": "story",
"target_publish_date": "2026-03-22",
"video_url": "https://pub-xxx.r2.dev/videos/story-clip.mp4",
"instagram_audio_name": "Original Audio"
}'
If your balance is below 1 credit when adding a repost link:
{
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Not enough credits to complete this operation.",
"details": {
"reason": "Need 1 credit to add a story repost link.",
"feature": "repost_url"
}
}
}
YouTube Fields (Coming Soon)
YouTube support is not yet available. The following fields are reserved for future use:
| Field | Type | Description |
|---|---|---|
youtube_title | string | Video title. |
youtube_tags | string[] | Array of tags. |
youtube_category | string | Video category. |
youtube_visibility | string | "public", "unlisted", or "private". |
Examples
TikTok Video
curl -X PUT https://app.tokportal.com/api/ext/bundles/bundle_abc123/videos/1 \
-H "X-API-Key: sk_xxx" \
-H "Content-Type: application/json" \
-d '{
"video_type": "video",
"description": "Check out this new product! #ad #sponsored",
"target_publish_date": "2026-03-15",
"video_url": "https://pub-xxx.r2.dev/videos/bundle-id/promo.mp4",
"tiktok_sound_url": "https://www.tiktok.com/music/trending-sound-789",
"editing_instructions": "Add brand watermark in bottom-right corner",
"external_ref": "campaign-42-v1"
}'
Response:
{
"data": {
"position": 1,
"video_type": "video",
"status": "configured",
"description": "Check out this new product! #ad #sponsored",
"target_publish_date": "2026-03-15",
"video_url": "https://pub-xxx.r2.dev/videos/bundle-id/promo.mp4",
"tiktok_sound_url": "https://www.tiktok.com/music/trending-sound-789",
"editing_instructions": "Add brand watermark in bottom-right corner",
"external_ref": "campaign-42-v1"
}
}
TikTok Carousel
curl -X PUT https://app.tokportal.com/api/ext/bundles/bundle_abc123/videos/2 \
-H "X-API-Key: sk_xxx" \
-H "Content-Type: application/json" \
-d '{
"video_type": "carousel",
"description": "Swipe through our latest collection",
"target_publish_date": "2026-03-17",
"carousel_images": [
"carousel-images/org_xxx/bundle_abc123/slide1.jpg",
"carousel-images/org_xxx/bundle_abc123/slide2.jpg",
"carousel-images/org_xxx/bundle_abc123/slide3.jpg"
],
"tiktok_sound_url": "https://www.tiktok.com/music/chill-vibes-456",
"external_ref": "campaign-42-v2"
}'
Response:
{
"data": {
"position": 2,
"video_type": "carousel",
"status": "configured",
"description": "Swipe through our latest collection",
"target_publish_date": "2026-03-17",
"carousel_images": [
"carousel-images/org_xxx/bundle_abc123/slide1.jpg",
"carousel-images/org_xxx/bundle_abc123/slide2.jpg",
"carousel-images/org_xxx/bundle_abc123/slide3.jpg"
],
"tiktok_sound_url": "https://www.tiktok.com/music/chill-vibes-456",
"external_ref": "campaign-42-v2"
}
}
Instagram Reel (Video)
curl -X PUT https://app.tokportal.com/api/ext/bundles/bundle_def456/videos/1 \
-H "X-API-Key: sk_xxx" \
-H "Content-Type: application/json" \
-d '{
"video_type": "video",
"instagram_content_type": "reel",
"description": "Morning routine with our skincare line",
"target_publish_date": "2026-03-20",
"video_url": "https://pub-xxx.r2.dev/videos/bundle-id/reel-skincare.mp4",
"instagram_location": "Los Angeles, California",
"instagram_collaborators": ["brandofficial"],
"instagram_audio_name": "Original Audio",
"instagram_add_to_story": true,
"external_ref": "ig-reel-001"
}'
Instagram Reel Fixed Photos (creates a video from images, not swipeable)
curl -X PUT https://app.tokportal.com/api/ext/bundles/bundle_def456/videos/2 \
-H "X-API-Key: sk_xxx" \
-H "Content-Type: application/json" \
-d '{
"video_type": "carousel",
"instagram_content_type": "reel",
"description": "Style inspiration for spring",
"target_publish_date": "2026-03-20",
"carousel_images": [
"carousel-images/org_xxx/bundle_def456/slide1.jpg",
"carousel-images/org_xxx/bundle_def456/slide2.jpg"
],
"instagram_location": "Paris, France",
"instagram_collaborators": ["partner"],
"instagram_add_to_story": true
}'
Instagram Post Carousel (swipeable photo carousel)
curl -X PUT https://app.tokportal.com/api/ext/bundles/bundle_def456/videos/3 \
-H "X-API-Key: sk_xxx" \
-H "Content-Type: application/json" \
-d '{
"video_type": "carousel",
"instagram_content_type": "post",
"description": "Our latest collection",
"target_publish_date": "2026-03-20",
"carousel_images": [
"carousel-images/org_xxx/bundle_def456/photo1.jpg",
"carousel-images/org_xxx/bundle_def456/photo2.jpg"
],
"instagram_location": "Paris, France"
}'
INFO: Instagram Carousel Behavior For Instagram,
video_type: "carousel"behaves differently depending oninstagram_content_type:
- With
"reel": creates a video from your images (Fixed Photos — not swipeable)- With
"post": creates a swipeable photo carousel
TikTok Video with Sound Volume Control
Lower the original video sound and boost the added music. Costs 1 extra credit (charged once per video).
curl -X PUT https://app.tokportal.com/api/ext/bundles/bundle_abc123/videos/1 \
-H "X-API-Key: sk_xxx" \
-H "Content-Type: application/json" \
-d '{
"video_type": "video",
"description": "New product launch #ad",
"target_publish_date": "2026-03-15",
"video_url": "https://pub-xxx.r2.dev/videos/bundle-id/promo.mp4",
"tiktok_sound_url": "https://www.tiktok.com/music/trending-sound-789",
"volume_original_sound": 20,
"volume_added_sound": 150
}'
If your balance is below 1 credit:
{
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Not enough credits to complete this operation.",
"details": {
"reason": "Need 1 credit to enable sound volume control.",
"feature": "sound_volume"
}
}
}
TIP: Automatic Video Download You don't need to upload your video first. If you pass any external URL (Google Drive, Dropbox, any direct link) as
video_url, the API will automatically download the file and store it on our servers. The returnedvideo_urlin the response will be the final hosted URL. This also works in batch configuration and CSV import.URLs already hosted on our storage (
pub-0d3f...r2.dev) are kept as-is (no redundant re-upload).
Batch Configuration
Configure multiple videos in a single request. The body must be wrapped in { "videos": [...] }.
curl -X PUT https://app.tokportal.com/api/ext/bundles/bundle_abc123/videos/batch \
-H "X-API-Key: sk_xxx" \
-H "Content-Type: application/json" \
-d '{
"videos": [
{
"position": 1,
"video_type": "video",
"description": "Day 1 of the challenge #ad",
"target_publish_date": "2026-03-15",
"video_url": "https://pub-xxx.r2.dev/videos/bundle-id/day1.mp4"
},
{
"position": 2,
"video_type": "video",
"description": "Day 2 — things are getting interesting #ad",
"target_publish_date": "2026-03-18",
"video_url": "https://pub-xxx.r2.dev/videos/bundle-id/day2.mp4",
"tiktok_sound_url": "https://www.tiktok.com/music/hype-beat-101"
},
{
"position": 3,
"video_type": "carousel",
"description": "Final results — swipe to see the transformation",
"target_publish_date": "2026-03-21",
"carousel_images": [
"carousel-images/org_xxx/bundle_abc123/before.jpg",
"carousel-images/org_xxx/bundle_abc123/after.jpg"
],
"tiktok_sound_url": "https://www.tiktok.com/music/reveal-sound-202"
}
]
}'
Response:
{
"data": {
"bundle_id": "bundle_abc123",
"configured": 3,
"errors": [],
"results": [
{
"position": 1,
"video_type": "video",
"status": "configured"
},
{
"position": 2,
"video_type": "video",
"status": "configured"
},
{
"position": 3,
"video_type": "carousel",
"status": "configured"
}
]
}
}
If some videos fail validation, the response includes partial results:
{
"data": {
"bundle_id": "bundle_abc123",
"configured": 2,
"errors": [
{
"position": 3,
"error": "carousel_images is required when video_type is carousel"
}
],
"results": [
{
"position": 1,
"video_type": "video",
"status": "configured"
},
{
"position": 2,
"video_type": "video",
"status": "configured"
}
]
}
}
Date Validation
The target_publish_date must meet the following minimum lead times:
| Account Status | Minimum Lead Time |
|---|---|
| New (not yet delivered) | 3 days from today |
| Existing (already delivered) | 1 day from today |
If the date is too soon, the API returns a VALIDATION_ERROR:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "target_publish_date must be at least 3 days from today for new accounts.",
"details": {
"field": "target_publish_date",
"minimum_date": "2026-02-14"
}
}
}
Important: Media URL Types
When referencing uploaded files in video configuration:
video_url— Use thepublic_urlfrom the video upload response.carousel_images— Use thestorage_pathfrom the image upload response (notpublic_url).profile_picture_url— Use thestorage_pathfrom the image upload response (notpublic_url).
See Media Upload for details on uploading files and obtaining URLs.