Skip to Content
SdksTypeScript SDK

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/sdk

What do you want to do?

Jump straight to the task:


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! }) // 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/v1', // optional; defaults to production timeout: 30_000, // optional; ms maxRetries: 3, // optional; default 3 })
OptionTypeRequiredDescription
apiKeystringYesVoxBurst API key (vb_live_…)
baseUrlstringNoAPI base URL override. Must begin with https:// — see below
timeoutnumberNoRequest timeout in milliseconds
maxRetriesnumberNoNumber 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.

baseUrl must use HTTPS. As of @voxburst/sdk v1.3.0 the constructor throws immediately if baseUrl is set to anything not beginning with https://, so your API key cannot be sent over plaintext. There is no exemption for localhost. If you point the SDK at a local or proxied API, terminate TLS in front of it. Leaving baseUrl unset is unaffected — the default is already HTTPS.


Runtime compatibility

EnvironmentSupportNotes
Node.js✅ 18+Requires native fetch (built-in since Node 18). Polyfill with node-fetch on Node 16.
BunNative fetch built-in — no configuration needed.
DenoImport via npm specifier: npm:@voxburst/sdk.
Browser⚠️ PartialWorks in modern browsers but API keys are exposed client-side — use a server proxy in production.
Cloudflare WorkersNative fetch available.
Vercel / Next.js EdgeTested 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.


Which media path should I use?

My media is…What to doSDK method
At a public HTTPS URLRegister via REST, get a media IDPOST /v1/media/register (REST)
A local file on my serverRequest presigned URL, PUT bytes, poll until READYPOST /v1/media/upload (REST) → PUT → poll
Already in VoxBurstUse the mediaId you already havePass directly as media: ['media_abc...']
Needs platform validationUpload then call validateUpload → 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/
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:

StatusupdateretrydeleteMeaning
draftSaved but not queued
scheduledQueued for future publish
publishingIn flight — do not touch
publishedAll platforms succeeded
failedAll platforms failed
partialSome platforms failed
cancelledManually 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(`Retry queued — new status: ${retried.status}`) } 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[] } // list() only forwards `limit`/`cursor` to the API — there is no server-side `status` filter. const { data } = await client.accounts.list({ limit: 50 }) // [{ id: 'acc_instagram_abc', platform: 'INSTAGRAM', displayName: 'My Brand', username: 'mybrand', status: 'ACTIVE' }] // Filter by status client-side instead const activeAccounts = data.filter(a => a.status === 'ACTIVE') // 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 // Select the Page / organization to publish as → POST /v1/accounts/:id/select-page await client.accounts.selectPage('acc_abc123', 'page_456', { pageName: 'Acme Co' }) // Pass an empty pageId to clear the selection and publish as the personal profile (LinkedIn) await client.accounts.selectPage('acc_abc123', '') // 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.

selectPage() requires the accounts:write scope and throws ValidationError (HTTP 400) with a code you should branch on:

codeMeaning
PAGE_SELECTION_NOT_SUPPORTEDAccount was connected via Instagram Direct Login, which has no Facebook Pages. Reconnect via Facebook Login for Business first.
ACCOUNT_PAGE_MISMATCHThat Page belongs to a different Instagram account. Each Instagram account is a separate connected account — select the Page from the matching account’s own record.
DESTINATIONS_NOT_LOADEDDestinations are still being fetched — retry.

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 } const { uploadUrl, mediaId } = await client.media.getUploadUrl({ filename: 'photo.jpg', contentType: 'image/jpeg', sizeBytes: 204800, }) // Step 2 — upload to S3 directly. This is a raw fetch in every SDK: the presigned // URL must be sent without your API key, or S3 rejects the signature. 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)) media = await client.media.get(mediaId) } while (media.status === 'PENDING' || media.status === 'PROCESSING') if (media.status !== 'READY') throw new Error(`Media failed to process: ${mediaId}`) // Delete when no longer needed → DELETE /v1/media/:id await client.media.delete(mediaId) // 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.


client.analytics reference

The SDK wraps four analytics routes. The remaining routes are REST-only — see Analytics (REST-only) below.

// List post metrics across the workspace → GET /v1/analytics/posts // Response shape: { posts, has_more, next_cursor } — not { data, pagination } const { posts, has_more, next_cursor } = await client.analytics.listPostAnalytics({ startDate: '2026-05-01', endDate: '2026-05-31', platforms: ['INSTAGRAM', 'LINKEDIN'], limit: 50, }) // posts: PostAnalytics[] — each with { postId, platform, metrics: { impressions, reach, engagements, likes, ... }, dataAvailable, granularity } // Metrics for one post → GET /v1/analytics/posts/:id const { data: postMetrics } = await client.analytics.getPostAnalytics('post_abc123', { startDate: '2026-05-01', endDate: '2026-05-31', granularity: 'DAILY', // 'HOURLY' | 'DAILY' | 'WEEKLY' | 'MONTHLY' }) // Account-level metrics → GET /v1/analytics/accounts/:id const acct = await client.analytics.getAccountAnalytics('acc_abc123', { startDate: '2026-05-01', endDate: '2026-05-31', }) // acct.metrics: { followers, following, totalImpressions, totalReach, totalEngagements, totalPosts, engagementRate } // Workspace overview with period-over-period deltas → GET /v1/analytics/overview const overview = await client.analytics.getOverview({ startDate: '2026-05-01', endDate: '2026-05-31' }) // overview.summary, overview.comparison.deltas, overview.topPosts

Every metrics item carries a dataAvailable boolean. Check it before reading metrics — a platform that has not yet reported returns zeroed metrics rather than an error, so zeros are not necessarily real zeros.


Analytics (REST-only)

The remaining analytics routes are not wrapped by the SDK — use raw fetch with your API key. 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}` } // 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) { // next_cursor is typed `string | null`; list() expects `cursor?: string` — coerce null to undefined const page2 = await client.posts.list({ cursor: page1.pagination.next_cursor ?? undefined }) } // 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 control
  • listAll() — 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}`) } }
ClassHTTP statusWhen thrown
AuthenticationError401Missing or invalid API key
ValidationError400/422Invalid request; includes errors map
NotFoundError404Resource not found
RateLimitError429Rate limit exceeded; includes retryAfter seconds
ConflictError409Optimistic concurrency conflict on update
VoxBurstErrorall othersBase class for all API errors

Other resources

Most list methods follow the standard { data: T[], pagination: Pagination } envelope and return T directly for single-resource methods. client.contacts.list() is an exception — it returns { contacts: Contact[], total, page, limit, pages }, not { data, pagination }. Check each resource’s actual return type rather than assuming the standard envelope.

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) // Note the response shape: { contacts, total, page, limit, pages } — not { data, pagination } const { contacts } = await client.contacts.list({ search: 'alice', platform: 'TWITTER' }) const contact = await client.contacts.create({ platform: 'TWITTER', platformId: '...', displayName: 'Alice' }) await client.contacts.addTag('con_abc123', 'vip') await client.contacts.removeTag('con_abc123', 'vip') // contacts.get() returns ContactDetail — Contact plus recent engagement history const { contact: detail } = await client.contacts.get('con_abc123') detail.comments // ContactComment[] — comments they left on your posts detail.inboundMessages // ContactInboundMessage[] — DMs they sent you // Broadcasts (Agency plan) const broadcast = await client.broadcasts.create({ name: '...', messageTemplate: '...', platforms: ['TWITTER'] }) const { deliveries } = await client.broadcasts.listDeliveries('bc_abc123', { page: 1, limit: 50 }) // Sequences (Agency plan) const sequence = await client.sequences.create({ name: 'Onboarding Flow', exitOnReply: true }) const { steps } = await client.sequences.listSteps('seq_abc123') await client.sequences.enroll('seq_abc123', 'con_abc123', 'TWITTER') // exitEnrollment takes the ENROLLMENT id, not the contact id await client.sequences.exitEnrollment('seq_abc123', 'enroll_xyz789') // 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-workflows

ContactDetail.comments and ContactDetail.inboundMessages are truncated by the API and carry no cursor, so they are a recent-activity preview rather than a complete record. Do not treat an empty array as proof the contact has never commented or messaged you — page the inbox endpoints if you need the full history. The response also carries deliveries and sequenceEnrollments, which are not yet typed in the SDK.


Next steps

Last updated on