Scheduled campaign — end-to-end
Schedule a week of posts across multiple accounts in one pass. Uses batch.createPosts() with deterministic idempotency keys so the whole script is safe to re-run if it fails partway through.
Define your campaign posts
import { VoxBurstClient } from '@voxburst/sdk'
import crypto from 'crypto'
const client = new VoxBurstClient({ apiKey: process.env.VOXBURST_API_KEY! })
// Discover active accounts once
const { data: accounts } = await client.accounts.list({ status: 'ACTIVE' })
const twitter = accounts.find(a => a.platform === 'twitter')!
const linkedin = accounts.find(a => a.platform === 'linkedin')!
const bluesky = accounts.find(a => a.platform === 'bluesky')!
// One post per day for 5 days, sent to all three accounts
const campaignId = 'launch-week-2026-07'
const posts = [
{ content: 'Day 1: We shipped it. 🚀', slot: '2026-07-01T14:00:00Z' },
{ content: 'Day 2: Here\'s what we built.', slot: '2026-07-02T14:00:00Z' },
{ content: 'Day 3: The numbers so far 📊', slot: '2026-07-03T14:00:00Z' },
{ content: 'Day 4: What\'s next on the roadmap.', slot: '2026-07-04T14:00:00Z' },
{ content: 'Day 5: Thank you for the support 🙏', slot: '2026-07-05T14:00:00Z' },
]Build idempotency-keyed batch input
A deterministic key means you can safely re-run this script after a network failure — duplicate keys return the cached response instead of creating duplicate posts.
function idempotencyKey(campaignId: string, slot: string, accountIds: string[]) {
return crypto
.createHash('sha256')
.update(`${campaignId}:${slot}:${accountIds.sort().join(',')}`)
.digest('hex')
.slice(0, 64)
}
// batch.createPosts() accepts up to 50 items per call
const batchInput = posts.map(p => ({
content: p.content,
accountIds: [twitter.id, linkedin.id, bluesky.id],
scheduledFor: p.slot,
// Platform-specific overrides — LinkedIn gets a longer version
platformOverrides: {
LINKEDIN: {
content: `${p.content}\n\nFollow along this week as we share the story behind the build.`,
},
},
// Idempotency key per post (passed as metadata so the batch result is traceable)
metadata: {
idempotencyKey: idempotencyKey(campaignId, p.slot, [twitter.id, linkedin.id, bluesky.id]),
campaignId,
slot: p.slot,
},
}))batch.createPosts() itself is idempotent per item when you pass an Idempotency-Key header on the HTTP request. Storing the key in metadata also lets you look up which posts belong to a campaign later.
Submit the batch
const { results } = await client.batch.createPosts(batchInput)
const created = results.filter(r => r.status === 'created')
const failed = results.filter(r => r.status === 'failed')
console.log(`Created: ${created.length}/${posts.length}`)
if (failed.length > 0) {
console.error('Some posts failed to create:')
for (const f of failed) {
console.error(` index ${f.index}: ${JSON.stringify(f.errors)}`)
}
}
const postIds = created.map(r => r.postId!)Set up a webhook to receive results
Register once (not per campaign) to receive publish events for all posts.
// One-time setup — store endpoint.id and endpoint.secret
const endpoint = await client.webhooks.create({
url: 'https://yourapp.com/webhooks/voxburst',
events: ['post.published', 'post.failed', 'post.partial'],
})
console.log('Webhook secret (save this):', endpoint.secret)// Express webhook handler
import { createHmac, timingSafeEqual } from 'crypto'
import express from 'express'
const app = express()
app.use(express.raw({ type: 'application/json' }))
app.post('/webhooks/voxburst', (req, res) => {
// Verify signature
const sig = req.headers['x-voxburst-signature'] as string
const body = req.body as Buffer
const expected = createHmac('sha256', process.env.WEBHOOK_SECRET!)
.update(body).digest('hex')
if (!timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.sendStatus(401)
}
res.sendStatus(200) // ack before any async work
const event = JSON.parse(body.toString())
if (event.type === 'post.published') {
const post = event.data
console.log(`✅ Published: ${post.id} (${post.platforms[0]?.platformPostUrl})`)
}
if (event.type === 'post.failed' || event.type === 'post.partial') {
const post = event.data
const failed = post.platforms.filter((p: any) => p.status === 'FAILED')
for (const p of failed) {
console.error(`❌ ${post.id} / ${p.platform}: ${p.error?.message}`)
// Queue for remediation
remediationQueue.push({ postId: post.id, postPlatformId: p.postPlatformId })
}
}
})Recover partial failures
When a post publishes to 2 of 3 platforms, status is partial. Fix only the failed destinations — the successful ones are not re-sent.
async function fixFailedPlatforms(postId: string) {
const post = await client.posts.get(postId)
if (post.status !== 'partial' && post.status !== 'failed') {
console.log(`Post ${postId} is ${post.status} — nothing to fix`)
return
}
const toFix = post.platforms.filter(p => p.status === 'failed')
console.log(`Fixing ${toFix.length} failed platform(s) on ${postId}`)
for (const platform of toFix) {
console.log(` ${platform.platform}: ${platform.error?.message}`)
// fixPlatform is REST-only — use raw fetch
await fetch(`https://api.voxburst.io/v1/posts/${postId}/platforms/${platform.postPlatformId}/fix`, {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.VOXBURST_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ /* content: 'Adjusted version for this platform' */ }),
})
}
// Each fixed platform re-enters the publish queue independently
}Verify the full campaign after the week
// Pull all posts in this campaign by metadata tag
const allPosts = []
for await (const post of client.posts.listAll({ status: 'published' })) {
if (post.metadata?.campaignId === campaignId) {
allPosts.push(post)
}
}
const summary = {
total: allPosts.length,
fullyPublished: allPosts.filter(p => p.status === 'published').length,
partial: allPosts.filter(p => p.status === 'partial').length,
failed: allPosts.filter(p => p.status === 'failed').length,
}
console.table(summary)Related
- Batch API reference — limits, async batch jobs, dry-run mode
- Webhook events catalog — full payload shapes for every event
- Recovery playbook — diagnosing every category of failure
- Idempotency — key format, TTL, and conflict behavior