Media Upload

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 FieldUse This Value
video_urlpublic_url from the video upload response
carousel_images[]storage_path from the image upload response
profile_picture_urlstorage_path from the image upload response

Important: For carousel_images and profile_picture_url, use storage_pathnot public_url. For video_url, use public_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

  1. Request a presigned URL — Call the upload endpoint with file metadata.
  2. Upload the file — Use the returned upload_url and method to upload the file.
  3. 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.

FieldTypeRequiredDescription
filenamestringYesOriginal filename including extension (e.g., promo.mp4).
content_typestringYesMIME type of the file (e.g., video/mp4).
bundle_idstringYesThe 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.

FieldTypeRequiredDescription
filenamestringYesOriginal filename including extension (e.g., slide1.jpg).
content_typestringYesMIME type of the file (e.g., image/jpeg).
bundle_idstringYesThe bundle this image belongs to.
purposestringNo"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

FieldTypeDescription
upload_urlstringThe temporary presigned URL to upload the file to.
public_urlstringThe permanent public URL for the file after upload.
storage_pathstringThe storage path (images only). Use this for carousel_images and profile_picture_url.
methodstringHTTP method to use for the upload (always PUT).
content_typestringThe MIME type to set in the upload Content-Type header.
expires_in_secondsintegerSeconds until the presigned URL expires (typically 3600).
instructionsstringHuman-readable instructions for completing the upload.

Accepted Formats

Video

FormatMIME TypeExtension
MP4video/mp4.mp4
MOVvideo/quicktime.mov
WebMvideo/webm.webm

Image

FormatMIME TypeExtension
JPEGimage/jpeg.jpg, .jpeg
PNGimage/png.png
WebPimage/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."
  }
}