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: tok_live_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: tok_live_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" or "carousel". For Instagram: carousel with instagram_content_type: "reel" = Fixed Photos (creates a video from images), carousel with instagram_content_type: "post" = swipeable Carousel. |
description | string | Yes | Caption or description for the post. |
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". 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. |
Setting volume_original_sound or volume_added_sound on 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.
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 |
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: tok_live_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: tok_live_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: tok_live_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: tok_live_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: tok_live_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"
}'
For Instagram, video_type: "carousel" behaves differently depending on instagram_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: tok_live_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"
}
}
}
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 returned video_url in 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: tok_live_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.