Media Upload
Before configuring videos, you need to upload your media files (videos and images) to TokPortal's storage.
You have two options:
- Direct upload (simpler) — Send the file directly to the API. Best for scripts, MCP, and automation.
- Presigned URL (advanced) — Get a temporary upload URL and PUT the file yourself. Best for browser-based uploads.
Option 1: Direct Upload
Upload a file directly — the server handles everything.
Upload Video (Direct)
POST /upload/video/direct
Content-Type: multipart/form-data
curl -X POST https://app.tokportal.com/api/ext/upload/video/direct \
-H "X-API-Key: tok_live_xxx" \
-F "file=@/path/to/video.mp4" \
-F "bundle_id=your-bundle-id"
Response:
{
"data": {
"public_url": "https://pub-xxx.r2.dev/videos/bundle-id/123456.mp4",
"filename": "video.mp4",
"size_bytes": 15234567,
"content_type": "video/mp4"
}
}
Upload Image (Direct)
POST /upload/image/direct
Content-Type: multipart/form-data
curl -X POST https://app.tokportal.com/api/ext/upload/image/direct \
-H "X-API-Key: tok_live_xxx" \
-F "file=@/path/to/photo.jpg" \
-F "bundle_id=your-bundle-id" \
-F "purpose=carousel"
purpose can be carousel (default) or profile_picture.
Response:
{
"data": {
"public_url": "https://xxx.supabase.co/storage/v1/object/public/carousel-images/...",
"storage_path": "carousel-images/org_xxx/bundle-id/photo-123456.jpg",
"filename": "photo.jpg",
"size_bytes": 245678,
"content_type": "image/jpeg"
}
}
Using Upload Results in Video Configuration
| Video Config Field | Use This Value |
|---|---|
video_url | public_url from the video upload response |
carousel_images[] | storage_path from the image upload response |
profile_picture_url | storage_path from the image upload response |
Important: For
carousel_imagesandprofile_picture_url, usestorage_path— notpublic_url. Forvideo_url, usepublic_url.
Option 2: Presigned URL Upload
Get a temporary URL and upload the file yourself. This is useful for browser-based uploads where the client uploads directly.
Upload Flow
- Request a presigned URL — Call the upload endpoint with file metadata.
- Upload the file — Use the returned
upload_urlandmethodto upload the file. - Use the URL in video config — Pass the appropriate URL to the video configuration endpoints.
Presigned Video Upload
POST /upload/video
Returns a presigned Cloudflare R2 URL for uploading a video file.
| Field | Type | Required | Description |
|---|---|---|---|
filename | string | Yes | Original filename including extension (e.g., promo.mp4). |
content_type | string | Yes | MIME type of the file (e.g., video/mp4). |
bundle_id | string | Yes | The bundle this video belongs to. |
Step 1 — Get the presigned URL
curl -X POST https://app.tokportal.com/api/ext/upload/video \
-H "X-API-Key: tok_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"filename": "promo.mp4",
"content_type": "video/mp4",
"bundle_id": "bnd_a1b2c3d4"
}'
Response:
{
"data": {
"upload_url": "https://storage.tokportal.com/videos/org_xxx/bnd_a1b2c3d4/promo-1707660000.mp4?X-Amz-Algorithm=AWS4-HMAC-SHA256&...",
"public_url": "https://pub-xxx.r2.dev/videos/org_xxx/bnd_a1b2c3d4/promo-1707660000.mp4",
"method": "PUT",
"content_type": "video/mp4",
"expires_in_seconds": 3600,
"instructions": "PUT the file to upload_url with the specified Content-Type header."
}
}
Step 2 — Upload the file
Use the method and upload_url from the response:
curl -X PUT "https://storage.tokportal.com/videos/org_xxx/bnd_a1b2c3d4/promo-1707660000.mp4?X-Amz-Algorithm=AWS4-HMAC-SHA256&..." \
-H "Content-Type: video/mp4" \
--data-binary @promo.mp4
Step 3 — Use the public URL
Pass public_url as video_url when configuring a video:
{
"video_type": "video",
"video_url": "https://pub-xxx.r2.dev/videos/org_xxx/bnd_a1b2c3d4/promo-1707660000.mp4",
"description": "New product launch",
"target_publish_date": "2026-03-15"
}
Presigned Image Upload
POST /upload/image
Returns a presigned Supabase Storage URL for uploading an image. Use this for carousel slides and profile pictures.
| Field | Type | Required | Description |
|---|---|---|---|
filename | string | Yes | Original filename including extension (e.g., slide1.jpg). |
content_type | string | Yes | MIME type of the file (e.g., image/jpeg). |
bundle_id | string | Yes | The bundle this image belongs to. |
purpose | string | No | "carousel" (default) or "profile_picture". |
Step 1 — Get the presigned URL
curl -X POST https://app.tokportal.com/api/ext/upload/image \
-H "X-API-Key: tok_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"filename": "slide1.jpg",
"content_type": "image/jpeg",
"bundle_id": "bnd_a1b2c3d4",
"purpose": "carousel"
}'
Response:
{
"data": {
"upload_url": "https://xxx.supabase.co/storage/v1/object/upload/sign/carousel-images/org_xxx/bnd_a1b2c3d4/slide1-1707660000.jpg?token=...",
"public_url": "https://xxx.supabase.co/storage/v1/object/public/carousel-images/org_xxx/bnd_a1b2c3d4/slide1-1707660000.jpg",
"storage_path": "carousel-images/org_xxx/bnd_a1b2c3d4/slide1-1707660000.jpg",
"method": "PUT",
"content_type": "image/jpeg",
"expires_in_seconds": 3600,
"instructions": "PUT the file to upload_url with the specified Content-Type header."
}
}
Step 2 — Upload the file
curl -X PUT "https://xxx.supabase.co/storage/v1/object/upload/sign/carousel-images/org_xxx/bnd_a1b2c3d4/slide1-1707660000.jpg?token=..." \
-H "Content-Type: image/jpeg" \
--data-binary @slide1.jpg
Step 3 — Use the storage path
For carousel_images and profile_picture_url, use the storage_path value (not public_url):
{
"video_type": "carousel",
"carousel_images": [
"carousel-images/org_xxx/bnd_a1b2c3d4/slide1-1707660000.jpg",
"carousel-images/org_xxx/bnd_a1b2c3d4/slide2-1707660000.jpg"
],
"description": "Swipe through our latest collection",
"target_publish_date": "2026-03-15"
}
Presigned Upload Response Fields
| Field | Type | Description |
|---|---|---|
upload_url | string | The temporary presigned URL to upload the file to. |
public_url | string | The permanent public URL for the file after upload. |
storage_path | string | The storage path (images only). Use this for carousel_images and profile_picture_url. |
method | string | HTTP method to use for the upload (always PUT). |
content_type | string | The MIME type to set in the upload Content-Type header. |
expires_in_seconds | integer | Seconds until the presigned URL expires (typically 3600). |
instructions | string | Human-readable instructions for completing the upload. |
Accepted Formats
Video
| Format | MIME Type | Extension |
|---|---|---|
| MP4 | video/mp4 | .mp4 |
| MOV | video/quicktime | .mov |
| WebM | video/webm | .webm |
Image
| Format | MIME Type | Extension |
|---|---|---|
| JPEG | image/jpeg | .jpg, .jpeg |
| PNG | image/png | .png |
| WebP | image/webp | .webp |
Presigned URL Expiration
Presigned URLs expire after the time indicated in expires_in_seconds (typically 1 hour / 3600 seconds). If the URL expires before you upload, request a new one.
Error Handling
Unsupported file type:
{
"error": {
"code": "UNSUPPORTED_FILE_TYPE",
"message": "The content type 'video/avi' is not supported.",
"details": {
"supported_types": ["video/mp4", "video/quicktime", "video/webm"]
}
}
}
Missing required field:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "bundle_id is required."
}
}