TypeScript SDK
The @voxburst/sdk package is the official TypeScript/JavaScript SDK for the VoxBurst API. It provides full TypeScript support, automatic retries, cursor-based auto-pagination, and typed error classes.
npm install @voxburst/sdkWhat do you want to do?
Jump straight to the task:
- Post text to one platform
- Post an image to Instagram
- Schedule a post
- Post to multiple platforms at once
- Validate before posting
- Fetch post results
- Create many posts at once
- Recover from a rate limit
- Upload a local file
Build your first app
A guided path from installation to a published post.
import { VoxBurstClient } from '@voxburst/sdk'
const client = new VoxBurstClient({ apiKey: process.env.VOXBURST_API_KEY! })
const ctx = { signal: AbortSignal.timeout(30_000) }
// 1. Verify auth works
const { data: accounts } = await client.accounts.list()
console.log(`Connected accounts: ${accounts.length}`)
// [{ id: 'acc_twitter_abc', platform: 'twitter', displayName: 'My Brand' }, ...]
// 2. Find the account you want to post to
const instagram = accounts.find(a => a.platform === 'instagram')
if (!instagram) throw new Error('No Instagram account connected')
// 3. Validate your content before creating the post
const validation = await client.posts.validate({
content: 'Behind the scenes of our launch day 📸',
platforms: ['INSTAGRAM'],
})
if (!validation.valid) {
console.error('Validation failed:', validation.platforms)
process.exit(1)
}
// 4. Create a draft first
const draft = await client.posts.create({
content: 'Behind the scenes of our launch day 📸',
accountIds: [instagram.id],
contentType: 'IMAGE',
media: ['media_abc1234567890abc12345678'], // VoxBurst media ID
saveAsDraft: true,
})
console.log(`Draft created: ${draft.id} (status: ${draft.status})`)
// { id: 'post_abc123', status: 'draft', ... }
// 5. Schedule it
const scheduled = await client.posts.update(draft.id, {
scheduledFor: '2026-06-01T15:00:00Z',
saveAsDraft: false,
})
console.log(`Scheduled for: ${scheduled.scheduledFor}`)
// 6. Inspect results after it publishes
const result = await client.posts.get(draft.id)
console.log(`Status: ${result.status}`)
for (const p of result.platforms ?? []) {
console.log(` ${p.platform}: ${p.status} — ${p.platformPostUrl ?? 'pending'}`)
}
// 7. Handle an error cleanly
try {
await client.posts.get('post_doesnotexist')
} catch (err) {
if (err instanceof NotFoundError) console.log('Not found')
else if (err instanceof RateLimitError) console.log(`Rate limited — retry after ${err.retryAfter}s`)
else throw err
}Configuration
const client = new VoxBurstClient({
apiKey: process.env.VOXBURST_API_KEY!, // required
baseUrl: 'https://api.voxburst.io', // optional; defaults to production
timeout: 30_000, // optional; ms
maxRetries: 3, // optional; default 3
})| Option | Type | Required | Description |
|---|---|---|---|
apiKey | string | Yes | VoxBurst API key (vb_live_…) |
baseUrl | string | No | API base URL override |
timeout | number | No | Request timeout in milliseconds |
maxRetries | number | No | Number of retries on retriable errors (default: 3) |
The non-null assertion (!) is required in strict TypeScript. Store VOXBURST_API_KEY in your environment — never hardcode it.
Runtime compatibility
| Environment | Support | Notes |
|---|---|---|
| Node.js | ✅ 18+ | Requires native fetch (built-in since Node 18). Polyfill with node-fetch on Node 16. |
| Bun | ✅ | Native fetch built-in — no configuration needed. |
| Deno | ✅ | Import via npm specifier: npm:@voxburst/sdk. |
| Browser | ⚠️ Partial | Works in modern browsers but API keys are exposed client-side — use a server proxy in production. |
| Cloudflare Workers | ✅ | Native fetch available. |
| Vercel / Next.js Edge | ✅ | Tested on Vercel Edge Runtime and Next.js middleware. |
Module format: ships both ESM (import) and CJS (require). Tree-shaking works with ESM bundlers. No native addons.
Custom fetch: if your runtime lacks a global fetch, pass fetchImplementation:
import fetch from 'node-fetch'
const client = new VoxBurstClient({
apiKey: process.env.VOXBURST_API_KEY!,
fetchImplementation: fetch as any, // Node 16 polyfill
})Which media path should I use?
| My media is… | What to do | SDK method |
|---|---|---|
| At a public HTTPS URL | Register via REST, get a media ID | POST /v1/media/register (REST) |
| A local file on my server | Request presigned URL, PUT bytes, poll until READY | POST /v1/media/upload (REST) → PUT → poll |
| Already in VoxBurst | Use the mediaId you already have | Pass directly as media: ['media_abc...'] |
| Needs platform validation | Upload then call validate | Upload → POST /v1/media/:id/validate (REST) |
// Register a public URL (no upload needed)
const res = await fetch('https://api.voxburst.io/v1/media/register', {
method: 'POST',
headers: { 'Authorization': `Bearer ${process.env.VOXBURST_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ url: 'https://cdn.example.com/photo.jpg', contentType: 'image/jpeg' }),
})
const { id: mediaId } = await res.json()
// Use in a post
const post = await client.posts.create({
content: 'Check this out!',
accountIds: ['acc_instagram_abc'],
contentType: 'IMAGE',
media: [mediaId],
})Complete Instagram example
Instagram requires contentType and at least one media item on every post.
// Step 1: find the Instagram account
const { data: accounts } = await client.accounts.list()
const ig = accounts.find(a => a.platform === 'instagram')!
// Step 2: validate
const v = await client.posts.validate({
content: 'Launch day. Shipping now. 🚀',
platforms: ['INSTAGRAM'],
})
// { valid: true, platforms: { INSTAGRAM: { valid: true, errors: [], warnings: [] } } }
// Step 3: post
const post = await client.posts.create({
content: 'Launch day. Shipping now. 🚀',
accountIds: [ig.id],
contentType: 'IMAGE', // required for Instagram
media: ['media_abc1234567890abc12345678'], // pre-uploaded media ID
scheduledFor: '2026-06-01T15:00:00Z',
firstComment: '#launch #newproduct #buildinpublic',
firstCommentDelay: 60, // seconds after publish
})
console.log(post.id, post.status)
// post_abc123 scheduled
// Step 4: check result after it publishes
const result = await client.posts.get(post.id)
console.log(result.platforms?.[0]?.platformPostUrl)
// https://www.instagram.com/p/ABC123/Carousel
const carousel = await client.posts.create({
content: 'Swipe to see all the new features →',
accountIds: [ig.id],
contentType: 'CAROUSEL',
media: ['media_slide1', 'media_slide2', 'media_slide3'],
})Reel
const reel = await client.posts.create({
content: 'Our latest drop 🔥',
accountIds: [ig.id],
contentType: 'REEL',
media: ['media_video_abc123'],
})Cross-platform example
Post the same content to multiple platforms with per-platform overrides.
const { data: accounts } = await client.accounts.list()
const accountIds = accounts
.filter(a => ['instagram', 'twitter', 'linkedin'].includes(a.platform))
.map(a => a.id)
// Validate on all target platforms first
const v = await client.posts.validate({
content: 'Announcing VoxBurst 2.0 — unified social scheduling.',
platforms: ['INSTAGRAM', 'TWITTER', 'LINKEDIN'],
})
if (!v.valid) throw new Error(JSON.stringify(v.platforms))
const post = await client.posts.create({
content: 'Announcing VoxBurst 2.0 — unified social scheduling.',
accountIds,
contentType: 'IMAGE',
media: ['media_launch_image'],
platformOverrides: {
TWITTER: { content: 'VoxBurst 2.0 just shipped 🚀 #ProductLaunch #SocialMedia' },
LINKEDIN: { content: 'Today we are shipping VoxBurst 2.0. After 12 months of building, it is finally ready for teams that publish at scale across every channel.' },
},
firstComment: '#VoxBurst20 #Launch',
firstCommentDelay: 60,
})
// After publishing
const result = await client.posts.get(post.id)
// result.status: 'published' | 'partial' | 'failed'
for (const p of result.platforms ?? []) {
console.log(`${p.platform}: ${p.status} — ${p.platformPostUrl}`)
}
// Retry any failures
if (result.status === 'partial' || result.status === 'failed') {
await client.posts.retry(post.id)
}Post status lifecycle
Understanding which methods are valid from each status:
| Status | update | retry | delete | Meaning |
|---|---|---|---|---|
draft | ✅ | ✗ | ✅ | Saved but not queued |
scheduled | ✅ | ✗ | ✅ | Queued for future publish |
publishing | ✗ | ✗ | ✗ | In flight — do not touch |
published | ✗ | ✗ | ✗ | All platforms succeeded |
failed | ✅ | ✅ | ✅ | All platforms failed |
partial | ✅ | ✅ | ✗ | Some platforms failed |
cancelled | ✗ | ✗ | ✅ | Manually cancelled |
const post = await client.posts.get('post_abc123')
if (post.status === 'partial' || post.status === 'failed') {
const retried = await client.posts.retry(post.id)
console.log(`Retried: ${retried.retriedPlatforms?.length} platforms re-queued`)
}
if (post.status === 'failed') {
// Check per-platform errors
for (const p of post.platforms ?? []) {
if (p.status === 'failed') console.log(`${p.platform}: ${p.error}`)
}
}Validate before posting
const result = await client.posts.validate({
content: 'My post content here',
platforms: ['INSTAGRAM', 'TWITTER', 'LINKEDIN'],
})
// {
// valid: false,
// platforms: {
// INSTAGRAM: { valid: true, errors: [], warnings: [{ code: 'NO_MEDIA', message: '...' }] },
// TWITTER: { valid: false, errors: [{ code: 'CONTENT_TOO_LONG', message: '...' }], warnings: [] },
// LINKEDIN: { valid: true, errors: [], warnings: [] }
// }
// }
if (!result.valid) {
for (const [platform, r] of Object.entries(result.platforms)) {
if (!r.valid) console.error(`${platform}: ${r.errors.map(e => e.message).join(', ')}`)
}
}Get post results
const post = await client.posts.get('post_abc123')
// {
// id: 'post_abc123',
// content: '...',
// status: 'published',
// publishedAt: '2026-06-01T15:00:05Z',
// platforms: [
// { platform: 'instagram', status: 'published', platformPostUrl: 'https://www.instagram.com/p/...' },
// { platform: 'twitter', status: 'failed', error: 'CONTENT_TOO_LONG', retryCount: 1 }
// ]
// }Schedule a post
const post = await client.posts.create({
content: 'Launch day 🚀',
accountIds: ['acc_instagram_abc'],
contentType: 'IMAGE',
media: ['media_launch'],
scheduledFor: '2026-06-01T15:00:00Z',
})
// { id: 'post_abc123', status: 'scheduled', scheduledFor: '2026-06-01T15:00:00Z' }client.posts reference
// Create → POST /v1/posts → Post
await client.posts.create({
content, // string — required
accountIds, // string[] — required; platform inferred from each account
contentType?, // 'TEXT' | 'IMAGE' | 'VIDEO' | 'CAROUSEL' | 'STORY' | 'REEL' | 'THREAD'
media?, // string[] — VoxBurst media IDs
scheduledFor?, // ISO 8601 UTC string
saveAsDraft?, // boolean
firstComment?, // string
firstCommentDelay?,// number (0–60 seconds)
platformOverrides?,// Record<string, { content?: string; media?: string[] }>
queue?, // boolean — mutually exclusive with scheduledFor
tags?, // string[]
metadata?, // Record<string, unknown>
personaId?, // string
threadPosts?, // ThreadItem[]
isManualSave?, // boolean
})
// Read → GET /v1/posts/:id → Post
await client.posts.get(postId)
// List → GET /v1/posts → { data: Post[], pagination: Pagination }
await client.posts.list({ status?, platform?, from?, to?, limit?, cursor? })
// Auto-paginate all pages → AsyncIterable<Post>
for await (const post of client.posts.listAll({ status: 'published' })) {
console.log(post.id)
}
// Update → PATCH /v1/posts/:id → Post
await client.posts.update(postId, {
content?, scheduledFor?, accountIds?, media?,
platformOverrides?, firstComment?, firstCommentDelay?,
saveAsDraft?, tags?, metadata?, personaId?,
})
// Lifecycle operations
await client.posts.cancel(postId) // → POST /v1/posts/:id/cancel — cancel scheduled post; returns Post
await client.posts.retry(postId) // → POST /v1/posts/:id/retry — retry failed/partial; returns Post
await client.posts.unpublish(postId) // → POST /v1/posts/:id/unpublish — remove from platforms
await client.posts.delete(postId) // → DELETE /v1/posts/:id — draft/scheduled/failed only
// Note: publish(id), clone(id), fixPlatform(id, pid) are REST-only — not in the TypeScript SDK.
// For fixPlatform: POST /v1/posts/:id/platforms/:pid/fix
// Validate → POST /v1/posts/validate → ValidationResult
const v = await client.posts.validate({
content: '...',
platforms: ['INSTAGRAM', 'TWITTER'], // UPPERCASE platform constants
})
// v.valid: boolean, v.platforms: Record<string, { valid: boolean, errors: string[] }>client.accounts reference
// List → GET /v1/accounts → { data: Account[] }
const { data } = await client.accounts.list({ status?: 'ACTIVE' | 'EXPIRED' | 'DISCONNECTED' })
// [{ id: 'acc_instagram_abc', platform: 'instagram', displayName: 'My Brand', username: 'mybrand' }]
// Get → GET /v1/accounts/:id → Account
const account = await client.accounts.get('acc_abc123')
// Connect (OAuth) → POST /v1/accounts/connect/:platform → { url: string }
const { url } = await client.accounts.initiateConnect('TWITTER', 'https://yourapp.com/oauth/callback')
// Redirect the user to `url` to complete OAuth
// Maintenance
await client.accounts.refresh('acc_abc123') // → POST /v1/accounts/:id/refresh — refresh OAuth tokens
await client.accounts.delete('acc_abc123') // → DELETE /v1/accounts/:id — disconnect account
// Note: completeConnect() and test() are REST-only — use raw fetch for the OAuth callback step.client.media reference
Media upload is a multi-step presign flow. The TypeScript SDK wraps the presign and polling steps:
// Step 1 — request an upload URL → POST /v1/media/upload → { uploadUrl, mediaId }
// (TypeScript SDK: use raw fetch for the S3 PUT; Go SDK has client.Media.RequestUploadURL)
const res = await fetch('https://api.voxburst.io/v1/media/upload', {
method: 'POST',
headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ filename: 'photo.jpg', contentType: 'image/jpeg', sizeBytes: 204800 }),
})
const { uploadUrl, mediaId } = await res.json()
// Step 2 — upload to S3 directly (no auth header)
await fetch(uploadUrl, { method: 'PUT', body: fileBlob, headers: { 'Content-Type': 'image/jpeg' } })
// Step 3 — poll until READY (status transitions: PENDING → PROCESSING → READY or FAILED)
let media
do {
await new Promise(r => setTimeout(r, 2000))
const r = await fetch(`https://api.voxburst.io/v1/media/${mediaId}`, { headers: { Authorization: `Bearer ${apiKey}` } })
media = await r.json()
} while (media.status === 'PENDING' || media.status === 'PROCESSING')
if (media.status !== 'READY') throw new Error(`Media failed: ${JSON.stringify(media.validationResults)}`)
// Use the mediaId in a post
await client.posts.create({ content: '...', accountIds: ['acc_123'], media: [mediaId] })See Media API reference for the full upload flow, validation results, and video requirements.
client.webhooks reference
// List → GET /v1/webhooks → { data: WebhookEndpoint[] }
const { data } = await client.webhooks.list()
// Create → POST /v1/webhooks → WebhookEndpoint (secret returned only here)
const endpoint = await client.webhooks.create({
url: 'https://yourapp.com/webhooks/voxburst',
events: ['post.published', 'post.failed'], // or ['*'] for all events
})
console.log(endpoint.secret) // save this — never returned again
// Get → GET /v1/webhooks/:id → WebhookEndpoint
await client.webhooks.get('wh_abc123')
// Update → PATCH /v1/webhooks/:id → WebhookEndpoint
await client.webhooks.update('wh_abc123', { events: ['post.published'], enabled: true })
// Delete → DELETE /v1/webhooks/:id → void
await client.webhooks.delete('wh_abc123')
// Note: webhooks.test() is REST-only — POST /v1/webhooks/test with raw fetch.See Webhooks API reference for payload shapes, signature verification, and retry behavior.
Analytics (REST-only)
@voxburst/sdk does not wrap analytics endpoints — use raw fetch with your API key. All 21 analytics routes follow the same pattern. See the Analytics API reference for the full endpoint list, query parameters, and response shapes.
const BASE = 'https://api.voxburst.io'
const headers = { Authorization: `Bearer ${process.env.VOXBURST_API_KEY}` }
// Per-post metrics
const postMetrics = await fetch(
`${BASE}/v1/analytics/posts/post_abc123?startDate=2026-05-01&endDate=2026-05-31`,
{ headers }
).then(r => r.json())
// { data: [{ date: '2026-05-01', impressions: 1200, likes: 45, ... }] }
// Account-level metrics
const acctMetrics = await fetch(
`${BASE}/v1/analytics/accounts/acc_abc123?startDate=2026-05-01&endDate=2026-05-31`,
{ headers }
).then(r => r.json())
// { followers: 12500, avgEngagementRate: 0.034, totalPosts: 42 }
// AI Insights (paid plans — requires system AI provider config)
const insights = await fetch(`${BASE}/v1/analytics/insights`, { headers }).then(r => r.json())
// { insights: [{ type, category, priority, text, detail, action, actionRoute }], ... }
// Hashtag performance
const hashtags = await fetch(
`${BASE}/v1/analytics/hashtag-performance?startDate=2026-05-01&endDate=2026-05-31&limit=10`,
{ headers }
).then(r => r.json())
// { hashtags: [{ tag, postCount, avgEngagementRate, totalImpressions, totalEngagements, topPlatforms }], ... }Pagination and batch guidance
// list() — one page at a time; use when you need control over pagination
const page1 = await client.posts.list({ status: 'published', limit: 20 })
if (page1.pagination.has_more) {
const page2 = await client.posts.list({ cursor: page1.pagination.next_cursor })
}
// listAll() — iterates all pages; use for data exports or processing everything
let count = 0
for await (const post of client.posts.listAll({ status: 'published' })) {
count++
// Each post is typed; safe to use immediately
}
console.log(`Total published posts: ${count}`)
// batch.createPosts() — create up to 50 posts in one request
const { results } = await client.batch.createPosts([
{ content: 'Post one', accountIds: ['acc_123'] },
{ content: 'Post two', accountIds: ['acc_456'], scheduledFor: '2026-06-01T10:00:00Z' },
])
// results: [{ id: 'post_abc1', status: 'created' }, { id: 'post_abc2', status: 'created' }]When to use what:
list()— when you need one page or want pagination controllistAll()— when you need every result (export, sync, reporting)batch.createPosts()— when creating many posts at once (content calendars, bulk import)
Production integration patterns
// Store the API key in environment — never hardcode
const client = new VoxBurstClient({ apiKey: process.env.VOXBURST_API_KEY! })
// Use timeout and maxRetries appropriate for your use case
const client = new VoxBurstClient({
apiKey: process.env.VOXBURST_API_KEY!,
timeout: 30_000, // 30s — increase for video upload operations
maxRetries: 3, // SDK retries automatically on 429 and 5xx
})
// Always validate before posting in production
async function safePost(content: string, accountIds: string[], platforms: string[]) {
const v = await client.posts.validate({ content, platforms })
if (!v.valid) throw new Error(`Validation failed: ${JSON.stringify(v.platforms)}`)
return client.posts.create({ content, accountIds })
}
// Handle rate limits explicitly
import { RateLimitError } from '@voxburst/sdk'
async function postWithBackoff(params: CreatePostInput, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await client.posts.create(params)
} catch (err) {
if (err instanceof RateLimitError && attempt < maxAttempts) {
await new Promise(r => setTimeout(r, (err.retryAfter ?? 60) * 1000))
continue
}
throw err
}
}
}Error handling
import {
VoxBurstError, ValidationError, AuthenticationError,
NotFoundError, RateLimitError, ConflictError,
} from '@voxburst/sdk'
try {
await client.posts.get('post_notfound')
} catch (err) {
if (err instanceof NotFoundError) {
console.log('Post not found')
} else if (err instanceof RateLimitError) {
console.log(`Rate limited — retry after ${err.retryAfter}s`)
} else if (err instanceof ValidationError) {
console.log('Validation failed:', err.errors)
} else if (err instanceof AuthenticationError) {
console.log('Invalid API key')
} else if (err instanceof ConflictError) {
console.log('Optimistic concurrency conflict — reload and retry')
} else if (err instanceof VoxBurstError) {
console.log(`API error ${err.status}: ${err.code} — ${err.message}`)
}
}| Class | HTTP status | When thrown |
|---|---|---|
AuthenticationError | 401 | Missing or invalid API key |
ValidationError | 400/422 | Invalid request; includes errors map |
NotFoundError | 404 | Resource not found |
RateLimitError | 429 | Rate limit exceeded; includes retryAfter seconds |
ConflictError | 409 | Optimistic concurrency conflict on update |
VoxBurstError | all others | Base class for all API errors |
Other resources
All resources follow the standard { data: T[], pagination: Pagination } envelope for list methods and return T directly for single-resource methods. The SDK does not rename response keys — client.contacts.list() returns { data: Contact[], pagination }, not { contacts }.
See the SDK ↔ REST mapping for the complete method-to-endpoint table for every resource.
// Batch
const { results } = await client.batch.execute([...])
const { results: posts } = await client.batch.createPosts([...])
// Contacts (CRM)
const { data: contacts } = await client.contacts.list({ search: 'alice', platform: 'TWITTER' })
const contact = await client.contacts.create({ platform: 'TWITTER', platformId: '...', displayName: 'Alice' })
// Broadcasts (Agency plan)
const broadcast = await client.broadcasts.create({ name: '...', messageTemplate: '...', platforms: ['TWITTER'] })
// Sequences (Agency plan)
const sequence = await client.sequences.create({ name: 'Onboarding Flow', exitOnReply: true })
// Note: Personas, Hashtag Sets, and Approval Workflows are REST-only — not in the TypeScript SDK.
// GET/POST /v1/personas — see /api-reference/personas
// GET/POST /v1/hashtag-sets — see /api-reference/hashtag-sets
// GET/POST /v1/approval-workflows — see /api-reference/approval-workflowsNext steps
- SDK ↔ REST mapping — complete method-to-endpoint table for every resource
- Getting Started — first API calls and environment setup
- API Reference — full REST endpoint documentation
- Python (REST) — no official Python SDK; REST-only with full examples
- MCP Server — let AI assistants manage your workspace
- Platform guides — platform-specific posting examples
- Common tasks — task-oriented recipes