Rendering & outputs
Starting a render run and getting the finished videos back.
Endpoints
| Method | Path | Description |
|---|---|---|
| POST | /campaigns/:id/ready | Transition draft → ready |
| POST | /campaigns/:id/launch | Start rendering all recipients |
| GET | /campaigns/:id/download | Download links for completed renders |
| GET | /campaigns/:id/export | Export results — ?format=json|csv&preset=default|sequencer |
| GET | /campaigns/:id/analytics | Views, completions, timeline |
ready and launch are documented in full under Campaign endpoints → Lifecycle actions, alongside the rest of the state machine. The three read endpoints below are documented here.
Typical render time
| Greeting-only | < 3 min |
| Greeting + PIP | < 5 min |
Reference
GET/campaigns/:id/download
Per-file download links for every completed render, with a suggested filename. For pulling the MP4s down rather than sending links.
Success: 200 OK
Only recipients in completed status appear — degraded, failed and cancelled rows are excluded, so totalFiles can be lower than the campaign's recipient count. filename is sanitized to [A-Za-z0-9.-] and is not guaranteed unique across recipients who share a name and company.
Path parameters
| Field | Type | Description |
|---|---|---|
| idreq | string (uuid) | Campaign id, in completed status. |
Response
200 OK
{
"data": {
"campaignName": "Q2 Outbound",
"totalFiles": 42,
"totalSizeBytes": 1234567890,
"files": [
{
"recipientId": "d4c3b2a1-...",
"firstName": "Sarah",
"company": "Acme",
"filename": "Sarah-Acme.mp4",
"videoUrl": "https://cdn.outvo.io/outputs/....mp4",
"shortId": "abc123",
"fileSize": 29876543,
"durationMs": 45000
}
]
}
}Errors
| Code | HTTP | When |
|---|---|---|
| CAMPAIGN_NOT_FOUND | 404 | Unknown id, not yours, or soft-deleted. |
| CAMPAIGN_NOT_READY | 409 | The campaign is not completed yet. Use the export endpoint for partial results while it renders. |
GET/campaigns/:id/export
Every recipient and its render result, as JSON or CSV, in either the full internal shape or the sequencer handoff contract.
Success: 200 OK
preset=sequencer returns the handoff contract — email, firstName, company, videoLink, videoThumbnail, videoStatus, degraded, expiresAt — with column names that import into a sequencer as variables without mapping. Branch your copy on degraded (greeting-only video, no site recording) and schedule every touch to land before expiresAt. A row with an empty videoLink never rendered at all; drop it.
videoLink resolves host-aware: a Vimeo-hosted render yields its Vimeo URL, never the raw share URL. videoThumbnail is an animated GIF of the recipient's own video, meant to be embedded as a clickable image linked to videoLink — see Email embed for the markup and the email-client rules it has to follow.
preset=default returns, per row: recipientId, firstName, company, title, url, email, externalId, status, degradationReason, videoUrl, shareableUrl, thumbnailUrl, shortId, durationMs, renderTimeMs, renderedAt, expiresAt — that is also the CSV header order. It grows additively, so parse CSV by header name, never by column position.
customFields is JSON-only. It is present on every format=json row of the default preset but is not a CSV column. A consumer that needs it must request format=json.Path parameters
| Field | Type | Description |
|---|---|---|
| idreq | string (uuid) | Campaign id. Any status — rows for unrendered recipients come back with empty video fields. |
Query parameters
| Field | Type | Description |
|---|---|---|
| format | string | CSV sets Content-Disposition to {campaign}-export.csv, or {campaign}-sequencer.csv for the sequencer preset.Values jsoncsvDefaultjson |
| preset | string | default is the full internal shape; sequencer is the handoff contract. Composes with format.Values defaultsequencerDefaultdefault |
Response
GET /campaigns/9a8b7c6d-.../export?preset=sequencer
{
"data": [
{
"email": "sarah@acme.com",
"firstName": "Sarah",
"company": "Acme",
"videoLink": "https://share.outvo.io/v/abc123",
"videoThumbnail": "https://share.outvo.io/v/abc123/thumb.gif",
"videoStatus": "completed",
"degraded": false,
"expiresAt": "2026-10-12T12:05:00.000Z"
}
],
"meta": { "total": 42, "campaignName": "Q2 Outbound", "preset": "sequencer" }
}Errors
| Code | HTTP | When |
|---|---|---|
| CAMPAIGN_NOT_FOUND | 404 | Unknown id, not yours, or soft-deleted. |
| VALIDATION_ERROR | 400 | An unknown format or preset. The message lists the valid values. |
GET/campaigns/:id/analytics
Aggregate engagement for a campaign, a 30-day timeline, and a per-recipient breakdown.
Success: 200 OK
timeline covers the last 30 days and is zero-filled: every day from the first recorded event through today is present, including quiet days, so bar spacing reflects elapsed time rather than only the days that happened to have activity. bookingClicks is present on every recipient row regardless of host.
uniqueViewers counts outputs that were opened, not distinct people — one person opening two recipients' videos counts twice, and the same person reopening one video counts once. It is the numerator of viewRate. The raw totalViews/totalPlayed/totalCompleted counters, by contrast, count individual events, so they can exceed the output-based figures.
viewRate,playRateandcompletionRateare scoped to non-Vimeo outputs and arenull— not0— for a campaign that is entirely Vimeo-hosted.vimeoPlaysis a separate, never-summed count; it does not feedtotalViews,totalPlayedortotalCompleted.limitedandpartialare both true whenever the campaign has any Vimeo-hosted output at all.- Per recipient,
hostis"play"or"vimeo". On a Vimeo row,viewsandcompletionsarenullandplayscarries the coarse count; on a play row it is the reverse.vimeoStatsFetchedAtdistinguishes "not synced yet" from "synced, genuinely zero".
Path parameters
| Field | Type | Description |
|---|---|---|
| idreq | string (uuid) | Campaign id. |
Response
200 OK
{
"data": {
"summary": {
"totalOutputs": 42,
"totalViews": 28,
"totalPlayed": 20,
"uniqueViewers": 25,
"totalCompleted": 18,
"viewRate": 63,
"playRate": 48,
"completionRate": 64,
"totalPlayOutputs": 40,
"totalVimeoOutputs": 2,
"vimeoPlays": 9,
"limited": true,
"partial": true
},
"timeline": [ { "date": "2026-08-01", "views": 5, "playedCount": 4, "completions": 3 } ],
"recipients": [
{
"recipientId": "d4c3b2a1-...",
"firstName": "Sarah",
"company": "Acme",
"shortId": "abc123",
"host": "play",
"vimeoUrl": null,
"plays": null,
"vimeoStatsFetchedAt": null,
"views": 2,
"completions": 1,
"bookingClicks": 0,
"lastViewed": "2026-08-12T09:14:00.000Z"
}
]
}
}Errors
| Code | HTTP | When |
|---|---|---|
| CAMPAIGN_NOT_FOUND | 404 | Unknown id, not yours, or soft-deleted. |
