Rendering & outputs

Starting a render run and getting the finished videos back.

Endpoints

MethodPathDescription
POST/campaigns/:id/readyTransition draft → ready
POST/campaigns/:id/launchStart rendering all recipients
GET/campaigns/:id/downloadDownload links for completed renders
GET/campaigns/:id/exportExport results — ?format=json|csv&preset=default|sequencer
GET/campaigns/:id/analyticsViews, 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

FieldTypeDescription
idreqstring (uuid)Campaign id, in completed status.

Response

json
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

CodeHTTPWhen
CAMPAIGN_NOT_FOUND404Unknown id, not yours, or soft-deleted.
CAMPAIGN_NOT_READY409The 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

FieldTypeDescription
idreqstring (uuid)Campaign id. Any status — rows for unrendered recipients come back with empty video fields.

Query parameters

FieldTypeDescription
formatstringCSV sets Content-Disposition to {campaign}-export.csv, or {campaign}-sequencer.csv for the sequencer preset.
ValuesjsoncsvDefaultjson
presetstringdefault is the full internal shape; sequencer is the handoff contract. Composes with format.
ValuesdefaultsequencerDefaultdefault

Response

json
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

CodeHTTPWhen
CAMPAIGN_NOT_FOUND404Unknown id, not yours, or soft-deleted.
VALIDATION_ERROR400An 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.

Mixed-host campaigns. A campaign can host some recipients on Vimeo, which reports only coarse play counts — no view or completion events. Rather than conflate the two:
  • viewRate, playRate and completionRate are scoped to non-Vimeo outputs and are null — not 0 — for a campaign that is entirely Vimeo-hosted.
  • vimeoPlays is a separate, never-summed count; it does not feed totalViews, totalPlayed or totalCompleted.
  • limited and partial are both true whenever the campaign has any Vimeo-hosted output at all.
  • Per recipient, host is "play" or "vimeo". On a Vimeo row, views and completions are null and plays carries the coarse count; on a play row it is the reverse. vimeoStatsFetchedAt distinguishes "not synced yet" from "synced, genuinely zero".

Path parameters

FieldTypeDescription
idreqstring (uuid)Campaign id.

Response

json
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

CodeHTTPWhen
CAMPAIGN_NOT_FOUND404Unknown id, not yours, or soft-deleted.