Media Upload – Videos & Images via Presigned URLs
Upload videos and images to TokPortal storage via presigned URLs. Supports direct upload and Google Drive import.
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: sk_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: sk_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.
Import Image From URL
If your image already exists at a public direct URL, TokPortal can fetch and store it permanently.
POST /upload/image/from-url
Content-Type: application/json
curl -X POST https://app.tokportal.com/api/ext/upload/image/from-url \
-H "X-API-Key: sk_xxx" \
-H "Content-Type: application/json" \
-d '{
"url": "https://cdn.example.com/slide1.jpg",
"bundle_id": "your-bundle-id",
"purpose": "carousel"
}'
Use the returned storage_path for carousel_images or profile_picture_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: sk_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: sk_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."
}
}