Async operations
Some work takes longer than a request should. Those endpoints hand you a receipt instead of a result — and you either poll it or let a webhook tell you.
The pattern
An async endpoint returns 202 Accepted with an id and a poll_url.
The 202 means we have taken the job, not the job is done. You then poll until the
status is terminal.
| Operation | Returns | Poll |
|---|---|---|
POST /brands/{brand}/media/from-url |
202 + job_id |
GET /brands/{brand}/jobs/{job} |
POST /brands/{brand}/reports/{report}/export |
202 + delivery_id |
GET /brands/{brand}/reports/deliveries/{delivery} |
POST /brands/{brand}/posts (schedule_type: now) |
201 + the post |
GET /brands/{brand}/posts/{post}/status |
Importing media from a URL
curl -X POST "https://vm.viraldashboard.io/api/public/v1/brands/412/media/from-url" \
-H "Authorization: Bearer $VD_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/banner.jpg", "asset_type": "image" }'
{
"data": {
"job_id": 91,
"status": "pending",
"poll_url": "https://vm.viraldashboard.io/api/public/v1/brands/412/jobs/91"
}
}
Poll it until status is completed or failed:
{
"data": {
"job_id": 91,
"status": "completed",
"workspace_asset_id": 8814,
"size_bytes": 148233,
"error": null
}
}
workspace_asset_id is the media asset you can now attach to a post. On failed, read
error.
The URL is fetched by our servers, so it must be publicly reachable. We refuse private and reserved
addresses (422 invalid_media_url). Uploading bytes you already hold? Skip the job entirely and
POST /brands/{brand}/media as multipart — that one is synchronous.
Exporting a report
curl -X POST "https://vm.viraldashboard.io/api/public/v1/brands/412/reports/18/export" \
-H "Authorization: Bearer $VD_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "format": "pdf", "start": "2026-06-12", "end": "2026-07-12" }'
That returns a delivery_id. Poll the delivery, and when status is sent
download the file:
curl "https://vm.viraldashboard.io/api/public/v1/brands/412/reports/deliveries/77/download" \
-H "Authorization: Bearer $VD_API_KEY" \
-o report.pdf
Download before it is ready and you get 409 report_not_ready — that is a "not yet", not a
failure. Keep polling.
Polling, without wasting your rate limit
Back off, and give up eventually. Do not spin:
async function waitForJob(brand, jobId, { timeoutMs = 120_000 } = {}) {
const started = Date.now()
let delay = 1000
for (;;) {
const res = await fetch(`${process.env.VD_API}/brands/${brand}/jobs/${jobId}`, {
headers: { Authorization: `Bearer ${process.env.VD_API_KEY}` },
})
const { data } = await res.json()
if (data.status === 'completed') return data
if (data.status === 'failed') throw new Error(data.error ?? 'job failed')
if (Date.now() - started > timeoutMs) throw new Error('timed out waiting for job')
await new Promise(r => setTimeout(r, delay))
delay = Math.min(delay * 1.5, 10_000) // ease off; cap at 10s
}
}
Better: do not poll at all
Polling burns rate limit to learn nothing most of the time. For publishing, subscribe to
post.published and post.failed and we will call you the moment it settles — zero
requests spent waiting. See
Webhooks.
A 202 is not a success. The most common integration bug here is treating the
accept as the outcome — marking a post "published" in your own database the moment the API returns.
The network can still reject it. Wait for the terminal status.