Skip to Content

Media

The Media API manages images, videos, documents, and caption files used in posts. All media is scoped to a workspace. The standard upload flow uses presigned S3 PUT URLs — your file bytes go directly to storage without passing through VoxBurst’s API servers.

Base URL

https://api.voxburst.io/v1/media

Supported File Types and Size Limits

CategoryMIME TypesMax Size
Imagesimage/jpeg, image/png, image/gif, image/webp20 MB
Videosvideo/mp4, video/quicktime, video/x-msvideo, video/webm500 MB
Documentsapplication/pdf100 MB
Captionsapplication/x-subrip (.srt), text/vtt, text/plain5 MB

Bulk video upload accepts up to 20 videos with a combined batch cap of 20 GB.

PNG and WebP images are automatically converted to JPEG during processing. The stored file and contentType will reflect image/jpeg regardless of the original format. GIFs are preserved as-is to maintain animation. See Image Processing Pipeline below.


Media Status Values

StatusMeaning
PENDINGRecord created; upload not yet received
PROCESSINGUpload confirmed; processing in progress
READYFully processed — safe to attach to posts
FAILEDProcessing error — see validationResults

Async Google Drive and URL imports track their progress in metadata.importStatus — the media record itself remains in PENDING until the import completes and upload begins.


Upload Flow

The recommended upload flow is two steps:

  1. Request an upload URLPOST /v1/media/upload returns a presigned S3 PUT URL and a mediaId.
  2. PUT your file to the presigned URL directly. Processing begins automatically when the upload is received.

The presigned URL expires in 15 minutes. If your upload window expires before the PUT begins, call POST /v1/media/:id/refresh-presign to get a new URL.

// Step 1: request upload URL const res = await fetch('https://api.voxburst.io/v1/media/upload', { method: 'POST', headers: { 'Authorization': 'Bearer vb_live_xxxxxxxxxxxxx', 'Content-Type': 'application/json', }, body: JSON.stringify({ filename: 'banner.jpg', contentType: 'image/jpeg', sizeBytes: 204800, }), }) const { mediaId, uploadUrl } = await res.json() // Step 2: PUT file bytes directly to S3 await fetch(uploadUrl, { method: 'PUT', headers: { 'Content-Type': 'image/jpeg' }, body: fileBytes, }) // Poll GET /v1/media/:id until status === 'READY'

Once the media item reaches READY status, use its mediaId when creating or updating a post:

{ "content": "Check out our new product!", "media": ["media_abc1234567890abc12345678"] }

Request Upload URL

POST /v1/media/upload

Request body:

FieldTypeRequiredDescription
filenamestringYesFile name (1–255 chars)
contentTypestringYesMIME type — must be in the supported types table
sizeBytesintegerYesFile size in bytes (within the type limit)

Response (201):

{ "mediaId": "media_abc1234567890abc12345678", "uploadUrl": "https://s3.amazonaws.com/...", "uploadHeaders": { "Content-Type": "image/jpeg", "x-amz-server-side-encryption": "AES256" }, "expiresAt": "2026-04-23T13:05:00Z" }

Use all headers in uploadHeaders when making the PUT request to uploadUrl. Omitting required headers will cause the upload to be rejected by S3.

Upload URLs are short-lived (15 minutes), enforced for content-type and size, and tenant-isolated.


Get Media Item

GET /v1/media/:id

Returns a single media item. For video files, the response also includes videoDuration, videoCodec, videoWidth, videoHeight, and validationResults.

curl https://api.voxburst.io/v1/media/media_abc1234567890abc12345678 \ -H "Authorization: Bearer vb_live_xxxxxxxxxxxxx"

Response (200):

{ "id": "media_abc1234567890abc12345678", "filename": "banner.jpg", "contentType": "image/jpeg", "sizeBytes": 204800, "status": "READY", "publicUrl": "https://cdn.voxburst.io/...", "validationStatus": "skipped", "createdAt": "2026-04-23T12:00:00Z" }

List Media

GET /v1/media

Returns paginated media for the workspace.

Query parameters:

ParameterTypeDefaultDescription
limitinteger20Items per page (1–100)
cursorstringPagination cursor from previous response
statusstringFilter by status: pending, processing, ready, failed, or all
curl "https://api.voxburst.io/v1/media?status=ready&limit=50" \ -H "Authorization: Bearer vb_live_xxxxxxxxxxxxx"

Response (200):

{ "data": [ { "id": "media_abc1234567890abc12345678", "filename": "photo.jpg", "contentType": "image/jpeg", "sizeBytes": 204800, "status": "READY", "publicUrl": "https://cdn.voxburst.io/...", "createdAt": "2026-04-23T12:00:00Z" } ], "hasMore": false, "nextCursor": null }

Delete Media

DELETE /v1/media/:id

Required scopes: media:write

Permanently deletes the media record and the underlying file. This cannot be undone.

curl -X DELETE https://api.voxburst.io/v1/media/media_abc1234567890abc12345678 \ -H "Authorization: Bearer vb_live_xxxxxxxxxxxxx"

Response (200):

{ "success": true }

Deleting a media item that is already attached to a scheduled or published post does not remove it from those posts. The mediaId on the post record is preserved, but the file will no longer be accessible via publicUrl.


Refresh Presigned URL

POST /v1/media/:id/refresh-presign

Regenerates a presigned S3 PUT URL for a PENDING media record. Use this when the original 15-minute window expires before the upload begins.

Response (200):

{ "presignedUrl": "https://s3.amazonaws.com/...", "expiresAt": "2026-04-23T14:05:00Z" }

Confirm Upload

POST /v1/media/:id/confirm

Manually triggers processing for a media record. In normal operation this is not required — processing starts automatically when S3 receives your file. Call this endpoint only when the automatic trigger is unavailable (for example, in a local development environment without a live S3 event integration).

Response (200): Full media object reflecting the updated status.


Bulk Upload (Videos)

VoxBurst provides two bulk video upload endpoints for different workflows:

Bulk Presign (general)

POST /v1/media/bulk-presign

Required scopes: media:write

Request presigned PUT URLs for up to 20 video files at once. Presigned URLs expire in 60 minutes. Supports a wider range of video MIME types.

Request body:

FieldTypeRequiredDescription
filesarrayYes1–20 file descriptors
files[].filenamestringYesFile name (1–255 chars)
files[].contentTypestringYesvideo/mp4, video/quicktime, video/webm, video/x-msvideo, or video/avi
files[].sizeintegerYesFile size in bytes (max 500 MB per file)

Response (201):

{ "uploads": [ { "mediaId": "media_abc...", "presignedUrl": "https://s3.amazonaws.com/...", "key": "<upload-key>", "expiresAt": "2026-04-23T14:00:00Z" } ] }

Bulk Video Upload URLs (with validation)

POST /v1/media/bulk-video-upload-urls

Required scopes: media:write

Request presigned S3 PUT URLs for up to 20 videos with automatic platform-specific validation setup. Use this when you intend to publish videos and want validation results before creating posts. Presigned URLs expire in 60 minutes. Combined batch cap is 20 GB.

Request body:

FieldTypeRequiredDescription
videosarrayYes1–20 video descriptors
videos[].filenamestringYesFile name (1–255 chars)
videos[].contentTypestringYesvideo/mp4 or video/quicktime
videos[].fileSizeMbnumberYesFile size in megabytes (max 10 GB per file)
targetPlatformsstring[]YesPlatforms to validate against, e.g. ["INSTAGRAM", "TIKTOK"]
curl -X POST https://api.voxburst.io/v1/media/bulk-video-upload-urls \ -H "Authorization: Bearer vb_live_xxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "videos": [ { "filename": "product-demo.mp4", "contentType": "video/mp4", "fileSizeMb": 150 }, { "filename": "behind-scenes.mp4", "contentType": "video/mp4", "fileSizeMb": 80 } ], "targetPlatforms": ["INSTAGRAM", "TIKTOK"] }'

Response (201):

{ "videos": [ { "mediaId": "media_abc...", "uploadUrl": "https://s3.amazonaws.com/...", "s3Key": "<upload-key>", "expiresAt": "2026-04-23T14:00:00Z" } ] }

After uploading your files to the presigned URLs, each media record starts in PENDING status. Poll GET /v1/media/:id until status is READY, then use POST /v1/media/:id/validate to run platform-specific validation before attaching to posts.


Video Validation

Before attaching a video to a post, you can validate it against platform-specific constraints (resolution, aspect ratio, duration, codec, etc.).

Enqueue Validation

POST /v1/media/:id/validate

Required scopes: media:write

Returns 202 immediately. Poll GET /v1/media/:id/validation-status for results.

Request body:

FieldTypeRequiredDescription
targetPlatformsstring[]YesPlatforms to validate against, e.g. ["TIKTOK", "INSTAGRAM"]

Response (202):

{ "queued": true, "jobId": "job_abc123" }

If the validation queue is not configured for the workspace, the response is still 202 but with queued: false:

{ "queued": false, "reason": "validation queue not configured" }

Get Validation Status

GET /v1/media/:id/validation-status

{ "id": "media_abc1234567890abc12345678", "validationStatus": "passed", "validationResults": { "TIKTOK": { "valid": true }, "INSTAGRAM": { "valid": false, "errors": ["Resolution too low"] } }, "videoDuration": 45, "videoCodec": "h264", "videoWidth": 1920, "videoHeight": 1080 }
validationStatusMeaning
pendingAwaiting validation
passedVideo meets all platform constraints
failedVideo fails one or more platform constraints
skippedValidation not run for this item

Register External Media

POST /v1/media/register

Register a media file that is already publicly accessible at an external URL. VoxBurst does not re-host the file — the URL is stored as-is. Use this when the file is hosted on your own CDN and you want to reference it in posts.

Required scopes: media:write

Request Body

FieldTypeRequiredDescription
urlstringYesPublic HTTPS URL of the media file
contentTypestringYesMIME type of the file
filenamestringNoOptional display filename
curl -X POST https://api.voxburst.io/v1/media/register \ -H "Authorization: Bearer vb_live_xxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "url": "https://cdn.yourapp.com/image.jpg", "contentType": "image/jpeg", "filename": "campaign-banner.jpg" }'

Response (201): Full media object.


Bulk Validate

POST /v1/media/bulk-validate

Enqueue platform validation for multiple media items at once.

Required scopes: media:write

Request Body

FieldTypeRequiredDescription
mediaIdsstring[]YesIDs of media records to validate
platformsstring[]YesPlatform constants to validate against
curl -X POST https://api.voxburst.io/v1/media/bulk-validate \ -H "Authorization: Bearer vb_live_xxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "mediaIds": ["media_abc123", "media_def456"], "platforms": ["INSTAGRAM", "TIKTOK"] }'

Response (202):

{ "jobIds": ["job_abc123", "job_def456"] }

Import from URL

POST /v1/media/import-from-url

This endpoint is for authenticated sources only (e.g. Google Photos, private storage). It passes the accessToken as a Bearer header when downloading the file. For public CDN URLs that require no authentication, use Register External Media instead.

Async import a file from an authenticated URL into VoxBurst storage. Returns immediately with a mediaId — poll GET /v1/media/:id until status is READY.

Required scopes: media:write

Request Body

FieldTypeRequiredDescription
downloadUrlstringYesURL to download the file from
filenamestringYesDisplay filename for the imported file
mimeTypestringYesExpected MIME type of the file
accessTokenstringYesBearer token to include when fetching the URL
curl -X POST https://api.voxburst.io/v1/media/import-from-url \ -H "Authorization: Bearer vb_live_xxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "downloadUrl": "https://example.com/video.mp4", "filename": "product-demo.mp4", "mimeType": "video/mp4", "accessToken": "your_bearer_token_for_the_source_url" }'

Response (202): { "mediaId": "media_abc...", "jobId": "...", "status": "importing" }


Drive Import (Legacy Sync)

POST /v1/media/drive-import

Synchronously import files from Google Drive into VoxBurst storage. Unlike the async import, this endpoint processes all files before responding. Returns HTTP 207 with per-file results.

Required scopes: media:write

Request Body

FieldTypeRequiredDescription
filesarrayYesFiles to import
files[].driveFileIdstringYesGoogle Drive file ID
files[].namestringYesDisplay filename
files[].mimeTypestringYesMIME type of the file
files[].sizeBytesintegerYesFile size in bytes
accessTokenstringYesGoogle OAuth access token with Drive read scope

Response (207):

{ "results": [ { "fileId": "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs", "mediaId": "media_abc...", "url": "https://cdn.voxburst.io/...", "status": "ok" } ] }

Google Drive Import

Async Import from Google Drive

POST /v1/media/import-from-drive

Required scopes: media:write

Starts an async import from Google Drive to VoxBurst storage. Returns immediately with a jobId. Poll GET /v1/media/import/:jobId/status every 2 seconds until status is complete or failed.

Request body:

FieldTypeRequiredDescription
fileIdstringYesGoogle Drive file ID
filenamestringYesDisplay filename (1–255 chars)
mimeTypestringYesFile MIME type
accessTokenstringYesGoogle OAuth access token with Drive read scope
sizeBytesintegerNoFile size hint in bytes

Response (202):

{ "mediaId": "media_abc...", "jobId": "job_xyz", "status": "importing" }

Import Job Status

GET /v1/media/import/:jobId/status

Response (200):

{ "mediaId": "media_abc...", "jobId": "job_xyz", "status": "complete", "url": "https://cdn.voxburst.io/...", "contentType": "image/jpeg" }
statusMeaning
importingStill in progress
completeFile is ready; url is set
failedImport failed; error field contains the reason

Webhook Events

Subscribe to media events via POST /v1/webhooks:

EventFired when
media.uploadedUpload confirmed and processing has started

media.ready and media.processing_failed are internal events and are not subscribable via the webhooks API. To track when a media file finishes processing, poll GET /v1/media/:id until status is READY or FAILED.


Image Processing Pipeline

All images uploaded to VoxBurst — whether via direct upload, Google Drive import, or AI generation — pass through a normalization pipeline before being marked READY. The goal is a single, platform-safe image format across all publishing destinations.

What happens during normalization

  1. Format detection — The raw file bytes are inspected (magic bytes, not the declared MIME type) to determine the actual image format.
  2. JPEG conversion — If the image is PNG or WebP, it is transcoded to a baseline JPEG using Sharp . Transparent areas are composited onto a white background before conversion.
  3. JFIF header injection — Sharp’s libvips engine (used on AWS Lambda/ARM64) produces DQT-first JPEG streams (FF D8 FF DB) that some platforms — notably the Instagram Graph API — reject. A standards-compliant 18-byte JFIF APP0 marker block is injected between the SOI and the first DQT marker, producing a FF D8 FF E0 (JFIF) stream that all platforms accept.
  4. S3 overwrite — The normalised bytes overwrite the original file at the same S3 key with ContentType: image/jpeg. The publicUrl and storagePath on the media record do not change.
  5. Record updatecontentType is set to image/jpeg and sizeBytes is updated to reflect the transcoded file.

Format handling summary

Input formatOutputNotes
JPEG (image/jpeg)JPEG (JFIF-compliant)JFIF header injected if missing; no re-encode
PNG (image/png)JPEGTransparency composited onto white background
WebP (image/webp)JPEGLossless and animated WebP both supported
GIF (image/gif)GIF (unchanged)Animation is preserved; GIFs are never transcoded

Why JPEG everywhere?

Social media platforms have strict and inconsistent image format requirements. Instagram’s Graph API requires JFIF-compliant JPEG. Twitter/X, LinkedIn, and Pinterest all prefer or require JPEG for photos. Standardising on JPEG at ingest time eliminates per-platform format logic and ensures that every image in your media library is publication-ready.

AI-generated images

Images generated by VoxBurst’s AI Generation feature (Stable Diffusion, DALL·E, Fal.ai, etc.) go through the same pipeline. AI providers typically return progressive mozjpeg-encoded JPEG streams or PNG files — both of which are normalised to baseline JFIF JPEG on ingest. The contentType returned from the AI generation API endpoints is always image/jpeg.

Re-publishing failed posts

When you use the Refresh Media & Retry action on a failed post, the original media file is copied to a new S3 key and passed through the same normalization pipeline before the retry is enqueued. This is specifically designed to recover from Instagram error 9004 (media rejected by Meta’s servers) caused by non-JFIF JPEG streams.

Normalization failures (for example, a corrupted image that Sharp cannot decode) are logged as warnings and do not block the upload. The original file is preserved and the media record is still created. Check validationResults if a post fails due to media errors.


Error Codes

CodeHTTPDescription
NOT_FOUND404No media item with that ID in this workspace
VALIDATION_ERROR400Invalid contentType, filename, or sizeBytes — check the message field for which constraint failed

Triggering video validation on a media item when the validation queue is not configured returns 202 with { "queued": false, "reason": "validation queue not configured" } rather than an error.

Last updated on