Integration lifecycle
A complete, step-by-step reference for building a production-safe VoxBurst integration. Each step shows the API call, the expected response, and how to proceed.
Overview
A complete integration follows this sequence:
- Create and configure an API key
- Discover connected accounts
- Upload or register media (if needed)
- Validate the post before creating it
- Create a draft or scheduled post
- Track publish status (poll or webhooks)
- Handle partial failures
- Retry transient errors
- Fix content or media errors
- Archive or unpublish when done
Step 1 — Create an API key
Go to VoxBurst dashboard → Settings → API and create a key with the scopes your integration needs.
| Minimum scope set for a scheduling integration |
|---|
accounts:read, posts:write, media:write, webhooks:write |
Store the key as an environment variable:
VOXBURST_API_KEY=vb_live_xxxxxxxxxxxxxVerify the key works:
curl https://api.voxburst.io/v1/health \
-H "Authorization: Bearer $VOXBURST_API_KEY"
# → 200 OKStep 2 — Discover accounts
Fetch the workspace’s connected accounts to find valid accountIds for posting.
curl "https://api.voxburst.io/v1/accounts?status=ACTIVE" \
-H "Authorization: Bearer $VOXBURST_API_KEY"Response:
{
"data": [
{
"id": "acc_abc123",
"platform": "instagram",
"username": "brandname",
"status": "active",
"accountType": "business"
},
{
"id": "acc_def456",
"platform": "twitter",
"username": "@brandname",
"status": "active"
}
],
"pagination": { "has_more": false, "next_cursor": null }
}Check before posting:
| Account status | What to do |
|---|---|
active | Ready to post |
expired | Call POST /v1/accounts/:id/refresh or prompt reconnect |
error | Check error details; likely needs reconnect |
disconnected | Must reconnect — cannot post |
Platform-specific prerequisites:
- Instagram: Account must be
businessorcreatortype. Personal accounts are not supported. - Facebook: Account must have a Page selected. Call
GET /v1/accounts/:id/pagesthenPOST /v1/accounts/:id/select-page. - Threads: Requires an Instagram Business/Creator account to be connected first.
Step 3 — Upload or register media (if needed)
Skip this step for text-only posts on platforms that support them. Instagram and YouTube always require media.
Option A — Upload a file
# 1. Request a presigned upload URL
curl -X POST https://api.voxburst.io/v1/media/upload \
-H "Authorization: Bearer $VOXBURST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"filename": "photo.jpg",
"contentType": "image/jpeg",
"sizeBytes": 204800
}'{
"mediaId": "media_abc1234567890abc12345678",
"uploadUrl": "https://s3.amazonaws.com/...",
"uploadHeaders": { "Content-Type": "image/jpeg", "x-amz-server-side-encryption": "AES256" },
"expiresAt": "2026-06-01T13:15:00Z"
}# 2. PUT the file bytes directly to S3 (use ALL uploadHeaders)
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: image/jpeg" \
-H "x-amz-server-side-encryption: AES256" \
--data-binary @photo.jpg# 3. Poll until READY (every 2-3 seconds)
curl https://api.voxburst.io/v1/media/media_abc1234567890abc12345678 \
-H "Authorization: Bearer $VOXBURST_API_KEY"Poll until "status": "READY". If "status": "FAILED", check validationResults for the reason.
Option B — Register an existing public URL
If the file is already on your CDN and publicly accessible:
curl -X POST https://api.voxburst.io/v1/media/register \
-H "Authorization: Bearer $VOXBURST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://cdn.yourapp.com/campaign-banner.jpg",
"contentType": "image/jpeg",
"filename": "campaign-banner.jpg"
}'The media record is created synchronously with "status": "READY" immediately.
Step 4 — Validate the post (optional but recommended)
Check content against platform rules before creating the post. This catches errors before they reach the social platforms.
curl -X POST https://api.voxburst.io/v1/posts/validate \
-H "Authorization: Bearer $VOXBURST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Check out our new launch! 🚀",
"platforms": ["INSTAGRAM", "TWITTER"],
"mediaIds": ["media_abc1234567890abc12345678"]
}'{
"valid": true,
"platforms": {
"INSTAGRAM": { "valid": true, "errors": [], "warnings": [] },
"TWITTER": {
"valid": true,
"errors": [],
"warnings": [{ "code": "NO_HASHTAGS", "message": "Posts with hashtags typically get more reach on Twitter" }]
}
}
}validate takes platform constants (["INSTAGRAM", "TWITTER"]), not accountIds. It checks platform rules, not account eligibility.
If valid: false for any platform, address the errors before proceeding.
Step 5 — Create the post
Create a scheduled post
curl -X POST https://api.voxburst.io/v1/posts \
-H "Authorization: Bearer $VOXBURST_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: create-post-campaign-a-slot-1" \
-d '{
"content": "Check out our new launch! 🚀",
"accountIds": ["acc_abc123", "acc_def456"],
"contentType": "IMAGE",
"media": ["media_abc1234567890abc12345678"],
"scheduledFor": "2026-06-15T14:00:00Z",
"platformOverrides": {
"TWITTER": { "content": "Just launched! 🚀 #voxburst" }
}
}'Always include an Idempotency-Key on post creation. This prevents duplicate posts if the request is retried after a network timeout.
Response (201):
{
"id": "post_xyz789",
"content": "Check out our new launch! 🚀",
"status": "scheduled",
"scheduledFor": "2026-06-15T14:00:00Z",
"platforms": [
{ "postPlatformId": "pp_111", "platform": "instagram", "accountId": "acc_abc123", "status": "pending" },
{ "postPlatformId": "pp_222", "platform": "twitter", "accountId": "acc_def456", "status": "pending" }
]
}Create a draft (no schedule)
Omit scheduledFor and the post is saved as "status": "draft".
Publish immediately
curl -X POST https://api.voxburst.io/v1/posts/post_xyz789/publish \
-H "Authorization: Bearer $VOXBURST_API_KEY"Step 6 — Track publish status
Option A — Poll
curl https://api.voxburst.io/v1/posts/post_xyz789 \
-H "Authorization: Bearer $VOXBURST_API_KEY"Poll every 5–10 seconds until status is no longer publishing. Terminal statuses are: published, partial, failed, cancelled, unpublished, archived.
Option B — Webhooks (recommended for production)
Subscribe to post.published and post.failed events to receive push notifications instead of polling:
curl -X POST https://api.voxburst.io/v1/webhooks \
-H "Authorization: Bearer $VOXBURST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/webhooks/voxburst",
"events": ["post.published", "post.failed", "account.error"]
}'The response includes a one-time secret for signature verification — copy it immediately.
Webhook payload for a partial publish:
{
"id": "evt_abc123",
"type": "post.published",
"createdAt": "2026-06-15T14:00:05Z",
"data": {
"id": "post_xyz789",
"status": "PARTIAL",
"platforms": [
{ "platform": "TWITTER", "status": "PUBLISHED", "platformPostUrl": "https://x.com/..." },
{ "platform": "INSTAGRAM", "status": "FAILED", "error": { "code": "MEDIA_REJECTED", "message": "..." } }
]
}
}There is no separate post.partial event. Partial failures arrive as post.published with status: "PARTIAL". Always check data.status in your post.published handler.
Verify every webhook signature:
import { createHmac, timingSafeEqual } from 'crypto'
function verifySignature(payload: string, signature: string, secret: string): boolean {
const parts = Object.fromEntries(signature.split(',').map(p => p.split('=', 2) as [string, string]))
const age = Math.floor(Date.now() / 1000) - parseInt(parts['t'], 10)
if (age > 300) return false // Reject replays > 5 minutes old
const expected = createHmac('sha256', secret).update(`${parts['t']}.${payload}`).digest('hex')
try {
return timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(parts['v2'], 'hex'))
} catch { return false }
}Complete webhook handler example
import { createHmac, timingSafeEqual } from 'crypto'
import express from 'express'
const app = express()
app.use(express.raw({ type: 'application/json' }))
function verifySignature(payload: Buffer, signature: string, secret: string): boolean {
const parts = Object.fromEntries(signature.split(',').map(p => p.split('=', 2) as [string, string]))
const age = Math.floor(Date.now() / 1000) - parseInt(parts['t'], 10)
if (age > 300) return false
const expected = createHmac('sha256', secret).update(`${parts['t']}.${payload}`).digest('hex')
try { return timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(parts['v2'], 'hex')) }
catch { return false }
}
// Deduplicate deliveries using delivery ID
const processedDeliveries = new Set<string>()
app.post('/webhooks/voxburst', (req, res) => {
// 1. Verify signature immediately
const sig = req.headers['x-voxburst-signature'] as string
if (!verifySignature(req.body, sig, process.env.WEBHOOK_SECRET!)) {
return res.status(401).send('Invalid signature')
}
// 2. Acknowledge immediately (before any async work)
res.status(200).send('OK')
// 3. Deduplicate
const deliveryId = req.headers['x-voxburst-delivery-id'] as string
if (processedDeliveries.has(deliveryId)) return
processedDeliveries.add(deliveryId)
// 4. Process asynchronously
const event = JSON.parse(req.body.toString())
handleEvent(event).catch(err => console.error('Webhook handler error:', err))
})
async function handleEvent(event: any) {
switch (event.type) {
case 'post.published': {
const { id, status, platforms } = event.data
if (status === 'PARTIAL') {
// Some platforms failed — trigger remediation
const failed = platforms.filter((p: any) => p.status === 'FAILED')
console.log(`Post ${id} partial: ${failed.length} platform(s) failed`)
await handlePartialFailure(id, failed)
} else {
console.log(`Post ${id} fully published`)
}
break
}
case 'post.failed':
console.log(`Post ${event.data.id} failed on all platforms`)
await scheduleRetry(event.data.id)
break
case 'account.error':
console.log(`Account ${event.data.accountId} needs reconnection`)
await notifyUserToReconnect(event.data.accountId)
break
}
}Step 7 — Handle partial failures
When status: "PARTIAL", at least one platform published and at least one failed.
- Fetch the post:
GET /v1/posts/:id - Inspect
platforms[]— checkstatusanderrorper platform - Decide: retry transient errors, fix content/media errors
Step 8 — Retry transient errors
Use retry for network errors, rate limits, and other transient failures:
curl -X POST https://api.voxburst.io/v1/posts/post_xyz789/retry \
-H "Authorization: Bearer $VOXBURST_API_KEY" \
-H "Idempotency-Key: retry-post_xyz789-attempt-1"VoxBurst re-queues only FAILED platforms (never re-posts platforms that already PUBLISHED). Retries use exponential backoff: 2 min → 4 min → 8 min. After 3 retries a platform is exhausted — use Fix (Step 9) to reset it.
Full failure recovery example
async function handlePartialFailure(postId: string, failedPlatforms: any[]) {
// Re-fetch the post to get postPlatformIds
const post = await fetch(`https://api.voxburst.io/v1/posts/${postId}`, {
headers: { 'Authorization': `Bearer ${process.env.VOXBURST_API_KEY}` }
}).then(r => r.json())
for (const failedPp of failedPlatforms) {
const platformRecord = post.platforms.find(
(p: any) => p.platform === failedPp.platform.toLowerCase()
)
if (!platformRecord) continue
const errorCode = failedPp.error?.code
if (errorCode === 'ASPECT_RATIO_INVALID' || errorCode === 'MEDIA_REJECTED') {
// Content/media error — fix with corrected media URL
await fetch(`https://api.voxburst.io/v1/posts/${postId}/platforms/${platformRecord.postPlatformId}/fix`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.VOXBURST_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ mediaUrl: 'https://cdn.yourapp.com/corrected-image.jpg' }),
})
} else {
// Transient error — retry with backoff
await fetch(`https://api.voxburst.io/v1/posts/${postId}/retry`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.VOXBURST_API_KEY}`,
'Idempotency-Key': `retry-${postId}-${Date.now()}`,
},
})
}
}
}Step 9 — Fix content or media errors
When a platform fails due to bad media or caption (not a transient error), use the Fix endpoint:
# Get the postPlatformId from the post's platforms[] array
curl -X POST https://api.voxburst.io/v1/posts/post_xyz789/platforms/pp_111/fix \
-H "Authorization: Bearer $VOXBURST_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "mediaUrl": "https://cdn.example.com/corrected-1080x1080.jpg" }'Fix resets the retry counter to 0 and resubmits immediately. Omit the body to resubmit with the original content.
Common errors and how to fix them:
| Error | Cause | Fix |
|---|---|---|
MEDIA_REJECTED / error 9004 | Instagram rejected image format | Re-upload a fresh JPEG or use /fix with a new URL |
ASPECT_RATIO_INVALID | Image ratio outside 4:5–1.91:1 | Re-crop to a supported ratio and use /fix |
CONTENT_TOO_LONG | Caption exceeded platform limit | PATCH /v1/posts/:id to shorten, then retry |
ACCOUNT_DISCONNECTED | OAuth token expired | Reconnect account; then retry |
QUOTA_EXCEEDED | YouTube daily limit (6/day) | Wait until midnight Pacific, then retry |
Step 10 — Archive or unpublish
Unpublish from VoxBurst (marks as unpublished, does not remove from platforms)
curl -X POST https://api.voxburst.io/v1/posts/post_xyz789/unpublish \
-H "Authorization: Bearer $VOXBURST_API_KEY"Archive (hides from default views)
Archive is not an explicit endpoint — it’s a status. Archived posts are hidden from default GET /v1/posts responses unless you filter with status=archived.
Error reference
All errors follow this shape:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Content exceeds maximum length for Twitter (280 characters)",
"details": { "field": "content", "platform": "TWITTER", "maxLength": 280, "actualLength": 312 }
}
}| HTTP | Code | Meaning | Retryable |
|---|---|---|---|
| 400 | VALIDATION_ERROR | Invalid request — fix the payload | No |
| 400 | BAD_REQUEST | Malformed request (e.g. invalid ID format) | No |
| 401 | UNAUTHORIZED | Missing or invalid API key | No |
| 403 | FORBIDDEN | Insufficient scope | No |
| 404 | NOT_FOUND | Resource not found | No |
| 409 | CONFLICT | Post status conflict (e.g. deleting a publishing post) | Check status |
| 409 | IDEMPOTENCY_CONFLICT | Key reused with different body | No |
| 422 | UNPROCESSABLE_ENTITY | Semantic validation failed | No |
| 429 | RATE_LIMITED | Rate limit exceeded — check Retry-After header | Yes |
| 500 | INTERNAL_ERROR | Server error | Yes (with backoff) |
Duplicate-safe retry pattern
async function createPostSafely(payload: object, idempotencyKey: string) {
for (let attempt = 0; attempt < 3; attempt++) {
try {
const res = await fetch('https://api.voxburst.io/v1/posts', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.VOXBURST_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey, // same key on every retry
},
body: JSON.stringify(payload),
})
if (res.status === 429) {
const retryAfter = parseInt(res.headers.get('Retry-After') ?? '5', 10)
await sleep(retryAfter * 1000)
continue
}
if (res.status >= 500) {
await sleep(Math.pow(2, attempt) * 1000)
continue
}
return await res.json()
} catch (err) {
if (attempt === 2) throw err
await sleep(Math.pow(2, attempt) * 1000)
}
}
}The same idempotencyKey is sent on every retry. If the first request succeeded but the response was lost, the second request returns the cached result rather than creating a duplicate post.