Advanced workflows
Copy-paste recipes for scenarios that go beyond basic post creation.
OAuth account connect + callback
Connect a new social account via OAuth. Two steps: initiate (get the authorization URL) → handle the callback (exchange the code for a connected account).
import { VoxBurstClient } from '@voxburst/sdk'
import express from 'express'
const client = new VoxBurstClient({ apiKey: process.env.VOXBURST_API_KEY! })
const app = express()
// Step 1 — initiate: redirect the user to the platform's OAuth page
app.get('/connect/:platform', async (req, res) => {
const platform = req.params.platform.toUpperCase() // e.g. 'TWITTER', 'LINKEDIN'
const callbackUrl = `${process.env.APP_URL}/oauth/callback/${platform.toLowerCase()}`
const { url } = await client.accounts.initiateConnect(platform, callbackUrl)
res.redirect(url)
})
// Step 2 — callback: exchange code via REST (completeConnect is not in the TypeScript SDK)
app.get('/oauth/callback/:platform', async (req, res) => {
const { code, state } = req.query as { code: string; state: string }
const platform = req.params.platform.toLowerCase()
try {
const r = await fetch(`https://api.voxburst.io/v1/accounts/callback/${platform}`, {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.VOXBURST_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ code, state }),
})
if (!r.ok) throw new Error(await r.text())
const account = await r.json()
console.log(`Connected: ${account.platform} — @${account.username} (${account.id})`)
res.redirect('/settings/accounts?connected=true')
} catch (err) {
console.error('OAuth callback failed:', err)
res.redirect('/settings/accounts?error=oauth_failed')
}
})The state parameter is required — it prevents CSRF. VoxBurst generates and validates it automatically. Do not modify or discard it between Step 1 and Step 2.
REST equivalent (curl):
# Step 1 — get authorization URL
curl -X POST https://api.voxburst.io/v1/accounts/connect/twitter \
-H "Authorization: Bearer $VOXBURST_API_KEY" \
-H "Content-Type: application/json" \
-d '{"callbackUrl": "https://yourapp.com/oauth/callback/twitter"}'
# → { "url": "https://twitter.com/i/oauth2/authorize?..." }
# Step 2 — complete (after the user authorizes and is redirected back)
curl -X POST https://api.voxburst.io/v1/accounts/callback/twitter \
-H "Authorization: Bearer $VOXBURST_API_KEY" \
-H "Content-Type: application/json" \
-d '{"code": "...", "state": "..."}'
# → Account objectWebhook HMAC signature verification
Every webhook delivery includes an X-VoxBurst-Signature header — a hex-encoded HMAC-SHA256 of the raw request body using your endpoint’s signing secret.
import { createHmac, timingSafeEqual } from 'crypto'
import express from 'express'
const app = express()
// Important: use raw body middleware (not JSON) — the signature is over raw bytes
app.use('/webhooks/voxburst', express.raw({ type: 'application/json' }))
app.post('/webhooks/voxburst', (req, res) => {
const body = req.body as Buffer
const sig = req.headers['x-voxburst-signature'] as string
if (!verifyWebhookSignature(body, sig, process.env.VOXBURST_WEBHOOK_SECRET!)) {
return res.status(401).json({ error: 'Invalid signature' })
}
// Ack immediately — process async to stay under the 10s delivery timeout
res.sendStatus(200)
const event = JSON.parse(body.toString())
handleEvent(event).catch(err => console.error('Event handler error:', err))
})
function verifyWebhookSignature(body: Buffer, sig: string, secret: string): boolean {
const expected = createHmac('sha256', secret).update(body).digest('hex')
try {
return timingSafeEqual(Buffer.from(sig, 'hex'), Buffer.from(expected, 'hex'))
} catch {
return false // length mismatch — definitely invalid
}
}
async function handleEvent(event: any) {
switch (event.type) {
case 'post.published':
console.log(`✅ ${event.data.id} published to ${event.data.platforms.length} platform(s)`)
break
case 'post.failed':
console.error(`❌ ${event.data.id} failed`)
for (const p of event.data.platforms.filter((p: any) => p.status === 'FAILED')) {
console.error(` ${p.platform}: ${p.error?.message}`)
}
break
case 'post.partial':
console.warn(`⚠️ ${event.data.id} partial — some platforms failed`)
break
case 'account.connected':
console.log(`🔗 New account connected: ${event.data.platform} @${event.data.username}`)
break
}
}The signing secret is shown only once — at webhook creation. Store it in a secret manager immediately. If lost, rotate it via PATCH /v1/webhooks/:id/rotate-secret.
Deduplication: webhook deliveries may retry on timeout. Use event.id as an idempotency key in your handler:
const processed = new Set<string>() // use Redis/DB in production
async function handleEvent(event: any) {
if (processed.has(event.id)) return // already handled
processed.add(event.id)
// ... handle event
}Partial publish failure → retry or fix
When a post’s status is partial, some platforms published and some failed. Use fixPlatform to resubmit only the failed destinations.
async function recoverPartialPost(postId: string) {
const post = await client.posts.get(postId)
if (post.status === 'failed') {
// All platforms failed — retry the whole post
console.log(`Retrying all platforms for ${postId}`)
return client.posts.retry(postId)
}
if (post.status === 'partial') {
const failed = post.platforms.filter(p => p.status === 'failed')
console.log(`Fixing ${failed.length} failed platform(s) on ${postId}`)
for (const platform of failed) {
const errorCode = platform.error?.code
if (errorCode === 'RATE_LIMITED' || errorCode === 'PLATFORM_UNAVAILABLE') {
// Transient — fix without content changes
// await client.posts.fixPlatform(postId, platform.postPlatformId)
} else if (errorCode === 'CONTENT_TOO_LONG' || errorCode === 'MEDIA_REQUIRED') {
// Content error — fix with an override for this platform
await fetch(, {
method: 'POST',
headers: { Authorization: , 'Content-Type': 'application/json' },
body: JSON.stringify({ content: truncateForPlatform(post.content, platform.platform) }),
})
} else {
console.error(`Unrecoverable error on ${platform.platform}: ${platform.error?.message}`)
}
}
}
}
function truncateForPlatform(content: string, platform: string): string {
const limits: Record<string, number> = { twitter: 280, bluesky: 300, threads: 500 }
const limit = limits[platform]
return limit && content.length > limit ? content.slice(0, limit - 3) + '...' : content
}Recurring posts
Create a post that repeats on a schedule. The post object gets isRecurring: true and generates child posts on each recurrence.
// Create a recurring post (weekly on Monday at 9am UTC)
const recurring = await client.posts.create({
content: 'Weekly reminder: check your analytics dashboard 📊',
accountIds: ['acc_twitter_abc', 'acc_linkedin_xyz'],
scheduledFor: '2026-07-07T09:00:00Z', // first occurrence
recurrence: {
frequency: 'WEEKLY',
interval: 1,
byDay: ['MO'], // Monday
until: '2026-12-31T00:00:00Z', // stop after this date; // REST-only — use raw fetch omit for indefinite
},
})
console.log(`Recurring post created: ${recurring.id} (isRecurring: ${recurring.isRecurring})`)
// List all posts generated by this recurring rule
const children = []
for await (const post of client.posts.listAll({ recurringPostId: recurring.id })) {
children.push(post)
}
console.log(`Generated ${children.length} scheduled posts`)
// Update the recurrence rule (affects future occurrences only)
await client.posts.update(recurring.id, {
recurrence: {
frequency: 'WEEKLY',
interval: 2, // change to every two weeks
byDay: ['MO'],
},
})
// Cancel a single occurrence without affecting others
const oneOff = children[2]
await client.posts.cancel(oneOff.id)
// Cancel the entire series
await client.posts.cancel(recurring.id)LinkedIn / Facebook page selection
LinkedIn and Facebook accounts may have multiple associated pages. List available pages and select one before posting.
// List pages via REST (listPages is not in the TypeScript SDK)
const pagesRes = await fetch('https://api.voxburst.io/v1/accounts/acc_linkedin_abc/pages', {
headers: { Authorization: `Bearer ${process.env.VOXBURST_API_KEY}` },
})
const { data: pages } = await pagesRes.json()
// pages: [{ pageId: 'page_123', name: 'Acme Corp', followerCount: 12400 }, ...]
// Select a default page via REST (selectPage is not in the TypeScript SDK)
await fetch('https://api.voxburst.io/v1/accounts/acc_linkedin_abc/select-page', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.VOXBURST_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ pageId: pages[0].pageId }),
})
// Post via SDK — page was set above
const post = await client.posts.create({
content: 'Announcing our new feature!',
accountIds: ['acc_linkedin_abc'],
})REST equivalent:
# List pages
curl "https://api.voxburst.io/v1/accounts/acc_linkedin_abc/pages" \
-H "Authorization: Bearer $VOXBURST_API_KEY"
# Select a default page
curl -X POST "https://api.voxburst.io/v1/accounts/acc_linkedin_abc/select-page" \
-H "Authorization: Bearer $VOXBURST_API_KEY" \
-H "Content-Type: application/json" \
-d '{"pageId": "page_123"}'YouTube playlist targeting
When posting a video to YouTube, optionally assign it to a playlist.
// List playlists via REST (listPlaylists is not in the TypeScript SDK)
const plRes = await fetch('https://api.voxburst.io/v1/accounts/acc_youtube_abc/playlists', {
headers: { Authorization: `Bearer ${process.env.VOXBURST_API_KEY}` },
})
const { data: playlists } = await plRes.json()
// playlists: [{ playlistId: 'PLabc123', title: 'Product Demos', videoCount: 8 }, ...]
// Assign the video to a playlist via platformOverrides
const post = await client.posts.create({
content: 'How to get started with VoxBurst in 5 minutes',
accountIds: ['acc_youtube_abc'],
contentType: 'VIDEO',
media: ['media_videoabc123'],
platformOverrides: {
YOUTUBE: {
playlistId: playlists[0].playlistId,
title: 'Getting Started with VoxBurst', // YouTube video title
description: 'Full walkthrough of the core features.',
visibility: 'PUBLIC', // 'PUBLIC' | 'PRIVATE' | 'UNLISTED'
categoryId: '28', // YouTube category ID (28 = Science & Technology)
},
},
})REST equivalent:
# List playlists
curl "https://api.voxburst.io/v1/accounts/acc_youtube_abc/playlists" \
-H "Authorization: Bearer $VOXBURST_API_KEY"Related
- Posts API reference — full field reference for
recurrence,platformOverrides, and lifecycle endpoints - Accounts API reference — connect, page selection, and account status
- Webhook events catalog — full payload shapes
- Recovery playbook — diagnosing every failure category