Skip to Content
GuidesIntegration lifecycle

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:

  1. Create and configure an API key
  2. Discover connected accounts
  3. Upload or register media (if needed)
  4. Validate the post before creating it
  5. Create a draft or scheduled post
  6. Track publish status (poll or webhooks)
  7. Handle partial failures
  8. Retry transient errors
  9. Fix content or media errors
  10. 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_xxxxxxxxxxxxx

Verify the key works:

curl https://api.voxburst.io/v1/health \ -H "Authorization: Bearer $VOXBURST_API_KEY" # → 200 OK

Step 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 statusWhat to do
activeReady to post
expiredCall POST /v1/accounts/:id/refresh or prompt reconnect
errorCheck error details; likely needs reconnect
disconnectedMust reconnect — cannot post

Platform-specific prerequisites:

  • Instagram: Account must be business or creator type. Personal accounts are not supported.
  • Facebook: Account must have a Page selected. Call GET /v1/accounts/:id/pages then POST /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.

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.

  1. Fetch the post: GET /v1/posts/:id
  2. Inspect platforms[] — check status and error per platform
  3. 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:

ErrorCauseFix
MEDIA_REJECTED / error 9004Instagram rejected image formatRe-upload a fresh JPEG or use /fix with a new URL
ASPECT_RATIO_INVALIDImage ratio outside 4:5–1.91:1Re-crop to a supported ratio and use /fix
CONTENT_TOO_LONGCaption exceeded platform limitPATCH /v1/posts/:id to shorten, then retry
ACCOUNT_DISCONNECTEDOAuth token expiredReconnect account; then retry
QUOTA_EXCEEDEDYouTube 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 } } }
HTTPCodeMeaningRetryable
400VALIDATION_ERRORInvalid request — fix the payloadNo
400BAD_REQUESTMalformed request (e.g. invalid ID format)No
401UNAUTHORIZEDMissing or invalid API keyNo
403FORBIDDENInsufficient scopeNo
404NOT_FOUNDResource not foundNo
409CONFLICTPost status conflict (e.g. deleting a publishing post)Check status
409IDEMPOTENCY_CONFLICTKey reused with different bodyNo
422UNPROCESSABLE_ENTITYSemantic validation failedNo
429RATE_LIMITEDRate limit exceeded — check Retry-After headerYes
500INTERNAL_ERRORServer errorYes (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.

Last updated on