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/mediaSupported File Types and Size Limits
| Category | MIME Types | Max Size |
|---|---|---|
| Images | image/jpeg, image/png, image/gif, image/webp | 20 MB |
| Videos | video/mp4, video/quicktime, video/x-msvideo, video/webm | 500 MB |
| Documents | application/pdf | 100 MB |
| Captions | application/x-subrip (.srt), text/vtt, text/plain | 5 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
| Status | Meaning |
|---|---|
PENDING | Record created; upload not yet received |
PROCESSING | Upload confirmed; processing in progress |
READY | Fully processed — safe to attach to posts |
FAILED | Processing 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:
- Request an upload URL —
POST /v1/media/uploadreturns a presigned S3 PUT URL and amediaId. - 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:
| Field | Type | Required | Description |
|---|---|---|---|
filename | string | Yes | File name (1–255 chars) |
contentType | string | Yes | MIME type — must be in the supported types table |
sizeBytes | integer | Yes | File 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:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 20 | Items per page (1–100) |
cursor | string | — | Pagination cursor from previous response |
status | string | — | Filter 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:
| Field | Type | Required | Description |
|---|---|---|---|
files | array | Yes | 1–20 file descriptors |
files[].filename | string | Yes | File name (1–255 chars) |
files[].contentType | string | Yes | video/mp4, video/quicktime, video/webm, video/x-msvideo, or video/avi |
files[].size | integer | Yes | File 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:
| Field | Type | Required | Description |
|---|---|---|---|
videos | array | Yes | 1–20 video descriptors |
videos[].filename | string | Yes | File name (1–255 chars) |
videos[].contentType | string | Yes | video/mp4 or video/quicktime |
videos[].fileSizeMb | number | Yes | File size in megabytes (max 10 GB per file) |
targetPlatforms | string[] | Yes | Platforms 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:
| Field | Type | Required | Description |
|---|---|---|---|
targetPlatforms | string[] | Yes | Platforms 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
}validationStatus | Meaning |
|---|---|
pending | Awaiting validation |
passed | Video meets all platform constraints |
failed | Video fails one or more platform constraints |
skipped | Validation 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
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | Public HTTPS URL of the media file |
contentType | string | Yes | MIME type of the file |
filename | string | No | Optional 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
| Field | Type | Required | Description |
|---|---|---|---|
mediaIds | string[] | Yes | IDs of media records to validate |
platforms | string[] | Yes | Platform 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
| Field | Type | Required | Description |
|---|---|---|---|
downloadUrl | string | Yes | URL to download the file from |
filename | string | Yes | Display filename for the imported file |
mimeType | string | Yes | Expected MIME type of the file |
accessToken | string | Yes | Bearer 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
| Field | Type | Required | Description |
|---|---|---|---|
files | array | Yes | Files to import |
files[].driveFileId | string | Yes | Google Drive file ID |
files[].name | string | Yes | Display filename |
files[].mimeType | string | Yes | MIME type of the file |
files[].sizeBytes | integer | Yes | File size in bytes |
accessToken | string | Yes | Google 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:
| Field | Type | Required | Description |
|---|---|---|---|
fileId | string | Yes | Google Drive file ID |
filename | string | Yes | Display filename (1–255 chars) |
mimeType | string | Yes | File MIME type |
accessToken | string | Yes | Google OAuth access token with Drive read scope |
sizeBytes | integer | No | File 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"
}status | Meaning |
|---|---|
importing | Still in progress |
complete | File is ready; url is set |
failed | Import failed; error field contains the reason |
Webhook Events
Subscribe to media events via POST /v1/webhooks:
| Event | Fired when |
|---|---|
media.uploaded | Upload 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
- Format detection — The raw file bytes are inspected (magic bytes, not the declared MIME type) to determine the actual image format.
- 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.
- 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 aFF D8 FF E0(JFIF) stream that all platforms accept. - S3 overwrite — The normalised bytes overwrite the original file at the same S3 key with
ContentType: image/jpeg. ThepublicUrlandstoragePathon the media record do not change. - Record update —
contentTypeis set toimage/jpegandsizeBytesis updated to reflect the transcoded file.
Format handling summary
| Input format | Output | Notes |
|---|---|---|
JPEG (image/jpeg) | JPEG (JFIF-compliant) | JFIF header injected if missing; no re-encode |
PNG (image/png) | JPEG | Transparency composited onto white background |
WebP (image/webp) | JPEG | Lossless 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
| Code | HTTP | Description |
|---|---|---|
NOT_FOUND | 404 | No media item with that ID in this workspace |
VALIDATION_ERROR | 400 | Invalid 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.