Skip to Content
GuidesPost with media — end-to-end

Post with media — end-to-end

A copy-paste-ready flow for posting an image or video. Covers every step from verifying your API key to recovering a failed platform publish.

This guide uses accountIds to target accounts — the platform is inferred from each account. platforms is only used in validate, not in create.

Verify auth and find your account

import { VoxBurstClient } from '@voxburst/sdk' const client = new VoxBurstClient({ apiKey: process.env.VOXBURST_API_KEY! }) const { data: accounts } = await client.accounts.list({ status: 'ACTIVE' }) const instagram = accounts.find(a => a.platform === 'instagram') if (!instagram) throw new Error('No active Instagram account connected') console.log(`Posting to: ${instagram.displayName} (${instagram.id})`)

Upload the file

Request a presigned URL, then PUT the file bytes directly to S3.

import { readFileSync } from 'fs' // Step 1 — request presigned URL const uploadRes = await fetch('https://api.voxburst.io/v1/media/upload', { method: 'POST', headers: { Authorization: `Bearer ${process.env.VOXBURST_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ filename: 'launch-photo.jpg', contentType: 'image/jpeg', sizeBytes: 512_000, }), }) const { mediaId, uploadUrl, uploadHeaders } = await uploadRes.json() // Step 2 — PUT bytes to S3 (no auth header — S3 handles auth via the signed URL) const file = readFileSync('./launch-photo.jpg') await fetch(uploadUrl, { method: 'PUT', headers: uploadHeaders, body: file, }) console.log(`Uploaded: ${mediaId}`)

Poll until READY

Media goes through PENDING → PROCESSING → READY (or FAILED). Poll every 2–3 seconds.

async function waitForMedia(mediaId: string, timeoutMs = 60_000) { const deadline = Date.now() + timeoutMs while (Date.now() < deadline) { const res = await fetch(`https://api.voxburst.io/v1/media/${mediaId}`, { headers: { Authorization: `Bearer ${process.env.VOXBURST_API_KEY}` }, }) const media = await res.json() if (media.status === 'READY') return media if (media.status === 'FAILED') { throw new Error(`Media processing failed: ${JSON.stringify(media.validationResults)}`) } await new Promise(r => setTimeout(r, 2500)) } throw new Error(`Media ${mediaId} did not become READY within ${timeoutMs}ms`) } const media = await waitForMedia(mediaId) console.log(`Ready: ${media.publicUrl}`)

Validate content against the target platform

const validation = await client.posts.validate({ content: 'Launch day 🚀 #product #buildinpublic', platforms: ['INSTAGRAM'], // UPPERCASE platform constants for validate }) if (!validation.valid) { for (const [platform, result] of Object.entries(validation.platforms)) { if (!result.valid) console.error(`${platform}: ${result.errors.join(', ')}`) } throw new Error('Validation failed — fix content before posting') }

Create the post with an idempotency key

The idempotency key makes this step safe to retry — a duplicate key + body returns the cached response without creating a second post.

import crypto from 'crypto' const content = 'Launch day 🚀 #product #buildinpublic' const scheduledFor = '2026-07-01T15:00:00Z' // Deterministic key: safe to re-run on failure const idempotencyKey = crypto .createHash('sha256') .update(`${instagram.id}:${content}:${scheduledFor}`) .digest('hex') .slice(0, 64) const post = await client.posts.create({ content, accountIds: [instagram.id], // platform inferred from account contentType: 'IMAGE', media: [mediaId], scheduledFor, firstComment: '#photography #voxburst', }) // With idempotency key (raw fetch — SDK exposes this via options in v1.3+) // Alternatively pass { headers: { 'Idempotency-Key': idempotencyKey } } in SDK v1.3+ console.log(`Post created: ${post.id} (status: ${post.status})`)

Track publish status

async function waitForPublish(postId: string, timeoutMs = 300_000) { const deadline = Date.now() + timeoutMs while (Date.now() < deadline) { const post = await client.posts.get(postId) if (['published', 'partial', 'failed'].includes(post.status)) return post await new Promise(r => setTimeout(r, 5000)) } throw new Error(`Post ${postId} did not finish within ${timeoutMs}ms`) } const result = await waitForPublish(post.id) for (const p of result.platforms) { const icon = p.status === 'published' ? '✅' : '❌' console.log(`${icon} ${p.platform}: ${p.platformPostUrl ?? p.error?.message}`) }

Fix a failed platform

If one platform failed (status partial), fix just that destination without re-posting to the platforms that succeeded.

const post = await client.posts.get(postId) const failed = post.platforms.filter(p => p.status === 'failed') for (const platform of failed) { console.log(`Fixing ${platform.platform}: ${platform.error?.message}`) // fixPlatform is REST-only — use raw fetch await fetch(`https://api.voxburst.io/v1/posts/${post.id}/platforms/${platform.postPlatformId}/fix`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.VOXBURST_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ /* content: 'Adjusted caption', mediaIds: [...] */ }), }) } // The post will re-enter the publish queue for the fixed platforms only

Last updated on