Campaign endpoints

The batch workflow: draft, assets, recipients, ready, launch.

Endpoints

MethodPathDescription
POST/campaignsCreate a new campaign
GET/campaignsList campaigns (paginated)
GET/campaigns/:idGet campaign detail + recipient stats
PATCH/campaigns/:idUpdate campaign (draft only)
DELETE/campaigns/:idSoft-delete campaign (any status except rendering)

A campaign moves draftready renderingcompleted (or failed / cancelled). Assets, recipients and settings are editable in draft only — every write endpoint below that says "draft only" answers 409 CAMPAIGN_NOT_DRAFT otherwise.

The campaign object

FieldTypeDescription
idstring (uuid)Campaign id.
userIdstring (uuid)Owner.
namestring1-200 characters.
descriptionstring | omittedFree text. Omitted when never set.
statusstringdraft, ready, rendering, completed, failed or cancelled.
Valuesdraftreadyrenderingcompletedfailedcancelled
placeholderWordstringThe word in the base recording that gets replaced. Empty string means the campaign has none.
greetingStartMsnumberStart of the greeting window in the base recording, in milliseconds.
Default0
greetingEndMsnumberEnd of the greeting window, in milliseconds.
Default3000
pipLayoutstringWhere the screen recording sits against the talking head. none renders greeting-only.
Valuesbottom-rightbottom-leftside-panelnoneDefaultbottom-right
pipDurationSecondsnumberScreen-recording length in seconds. 0 lets the pipeline decide.
Default0
videoHoststringResolved host. play is the Outvo player. Settable while draft only; immutable after launch.
Valuesplayvimeo
recordingModestringViewport used for the screen recording. Mobile records portrait.
ValuesdesktopmobileDefaultdesktop
scrollStartSecnumber | nullSeconds before scrolling begins. null uses the pipeline default.
Values0-30nullDefaultnull
scrollDownSecnumber | nullSeconds spent scrolling down.
Values0-30nullDefaultnull
scrollPauseSecnumber | nullSeconds paused at the bottom.
Values0-30nullDefaultnull
scrollUpSecnumber | nullSeconds spent scrolling back up.
Values0-30nullDefaultnull
disableScrollboolean | nullHold the recorded page still rather than scrolling it.
ValuestruefalsenullDefaulttrue
nameSpotlightboolean | nullEmphasize the name moment visually.
ValuestruefalsenullDefaultnull
liveRecordingOverlayboolean | nullDraw the live-recording chrome over the capture.
ValuestruefalsenullDefaultnull
deletedAtstring | omittedSet while soft-deleted. Restorable for 7 days.
recipientCountnumberPresent on list and detail.
completedCountnumberDetail only. Recipients in completed status.
failedCountnumberDetail only.
degradedCountnumberDetail only. Rendered without the screen recording — never billed.
cancelledCountnumberDetail only.
createdAtstring (ISO 8601)Creation timestamp.
updatedAtstring (ISO 8601)Last write timestamp.

Create, read, update, delete

POST/campaigns

Create a draft campaign. Everything except name is optional and can be PATCHed later.

Success: 201 Created

Headers

FieldTypeDescription
Idempotency-KeystringOptional, max 255 characters. A retry with the same key replays the original campaign instead of minting a duplicate — see Idempotency.

Body

FieldTypeDescription
namereqstring1-200 characters.
descriptionstringFree text.
placeholderWordstringThe word spoken in the base recording. Optional, and an empty string is legal — a campaign with no placeholder renders the base video with no name splice.
greetingStartMsnumberMilliseconds, minimum 0.
Default0
greetingEndMsnumberMilliseconds, minimum 0.
Default3000
pipLayoutstringWhere the screen recording sits against the talking head. none renders greeting-only.
Valuesbottom-rightbottom-leftside-panelnoneDefaultbottom-right
pipDurationSecondsnumberSeconds, minimum 0. 0 lets the pipeline decide.
Default0
videoHoststringFalls back to your account default, and to vimeo if you have not set one.
Valuesplayvimeo
recordingModestringViewport used for the screen recording. Mobile records portrait.
ValuesdesktopmobileDefaultdesktop
scrollStartSecnumber | nullSeconds before scrolling begins. null uses the pipeline default.
Values0-30nullDefaultnull
scrollDownSecnumber | nullSeconds spent scrolling down.
Values0-30nullDefaultnull
scrollPauseSecnumber | nullSeconds paused at the bottom.
Values0-30nullDefaultnull
scrollUpSecnumber | nullSeconds spent scrolling back up.
Values0-30nullDefaultnull
disableScrollboolean | nullSend false to scroll the recorded page. An explicit null also resolves to true.
ValuestruefalsenullDefaulttrue
nameSpotlightboolean | nullEmphasize the name moment visually.
ValuestruefalsenullDefaultnull
liveRecordingOverlayboolean | nullDraw the live-recording chrome over the capture.
ValuestruefalsenullDefaultnull

Request

json
POST /campaigns
{
  "name": "Q2 Outbound",
  "placeholderWord": "there",
  "pipLayout": "bottom-right",
  "pipDurationSeconds": 8
}

Response

json
201 Created

{
  "data": {
    "id": "9a8b7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d",
    "userId": "...",
    "name": "Q2 Outbound",
    "status": "draft",
    "placeholderWord": "there",
    "greetingStartMs": 0,
    "greetingEndMs": 3000,
    "pipLayout": "bottom-right",
    "pipDurationSeconds": 8,
    "videoHost": "vimeo",
    "recordingMode": "desktop",
    "scrollStartSec": null,
    "scrollDownSec": null,
    "scrollPauseSec": null,
    "scrollUpSec": null,
    "disableScroll": true,
    "nameSpotlight": null,
    "liveRecordingOverlay": null,
    "createdAt": "2026-08-13T12:00:00.000Z",
    "updatedAt": "2026-08-13T12:00:00.000Z"
  }
}

Errors

CodeHTTPWhen
VALIDATION_ERROR400Missing name, name outside 1-200 characters, or an enum/range violation.
IDEMPOTENCY_KEY_REUSED422Same Idempotency-Key, different body.

GET/campaigns

List your campaigns, newest first. Soft-deleted campaigns are excluded.

Success: 200 OK

List rows carry recipientCount but not the per-status counts — fetch the campaign by id for those.

Query parameters

FieldTypeDescription
pagenumberPage number, minimum 1.
Default1
per_pagenumberItems per page, maximum 100.
Default20
searchstringCase-insensitive substring match on name, max 200 characters. A blank value is not a filter. % and _ are matched literally; * behaves as a wildcard.

Response

json
200 OK

{
  "data": [ { "id": "...", "name": "Q2 Outbound", "status": "completed", "recipientCount": 42, ... } ],
  "meta": { "page": 1, "perPage": 20, "total": 7, "totalPages": 1 }
}

GET/campaigns/:id

Fetch one campaign with its live recipient breakdown.

Success: 200 OK

This endpoint self-heals a stuck campaign: if the status is still rendering while every recipient has reached a terminal state, it transitions to completed (or failed, if nothing succeeded) before responding. That path does not deliver a campaign.completed webhook, so do not use polling as a substitute for one.

Path parameters

FieldTypeDescription
idreqstring (uuid)Campaign id.

Response

json
200 OK

{
  "data": {
    "id": "...",
    "name": "Q2 Outbound",
    "status": "completed",
    "recipientCount": 42,
    "completedCount": 40,
    "failedCount": 1,
    "degradedCount": 1,
    "cancelledCount": 0,
    "...": "all campaign-object fields"
  }
}

Errors

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

PATCH/campaigns/:id

Partial update. Only the keys you send are written; draft campaigns only.

Success: 200 OK

Path parameters

FieldTypeDescription
idreqstring (uuid)Campaign id.

Request

json
PATCH /campaigns/9a8b7c6d-...
{ "placeholderWord": "there", "pipDurationSeconds": 10 }

Response

json
200 OK

{ "data": { /* the updated campaign object */ } }

Errors

CodeHTTPWhen
CAMPAIGN_NOT_FOUND404Unknown id, not yours, or soft-deleted.
CAMPAIGN_NOT_DRAFT409The campaign has left draft. Unready it first, if it is ready.
VALIDATION_ERROR400An enum or range violation.

DELETE/campaigns/:id

Soft-delete. The campaign disappears from lists and is purged for good after a 7-day grace period.

Success: 204 No Content

Any status except rendering can be deleted, not just drafts. Undo with POST /campaigns/:id/restore while the grace period lasts.

Path parameters

FieldTypeDescription
idreqstring (uuid)Campaign id.

Errors

CodeHTTPWhen
CAMPAIGN_NOT_FOUND404Unknown id, not yours, or already deleted.
CONFLICT409The campaign is rendering. Cancel it first — deleting would tear down in-flight jobs.

Lifecycle actions

Status-transition and recovery actions, all scoped to a single campaign. None of them takes a request body — send the POST without a Content-Type header (clone is the one exception).

MethodPathDescription
POST/campaigns/:id/readyTransition draft → ready
POST/campaigns/:id/unreadyTransition ready → draft
POST/campaigns/:id/launchStart rendering all recipients
POST/campaigns/:id/cancelCancel a rendering campaign
POST/campaigns/:id/retry-failedRe-queue every failed, degraded, cancelled and stuck recipient
POST/campaigns/:id/retry-recipient/:recipientIdRe-queue one recipient
POST/campaigns/:id/restoreRestore a soft-deleted campaign
POST/campaigns/:id/cloneDuplicate a campaign’s settings into a new draft

POST/campaigns/:id/ready

Lock the draft for launch. Verifies the campaign has a base video, that the video has finished processing, and that it has at least one recipient.

Success: 200 OK

Path parameters

FieldTypeDescription
idreqstring (uuid)Campaign id.

Response

json
200 OK

{ "data": { "id": "...", "status": "ready", ... } }

Errors

CodeHTTPWhen
CAMPAIGN_NOT_FOUND404Unknown id, not yours, or soft-deleted.
CAMPAIGN_NOT_DRAFT409Only a draft can become ready.
VALIDATION_ERROR400No base video uploaded, or no recipients added.
CAMPAIGN_NOT_READY409The base video is still being processed. Wait a few seconds and call again — this is transient, not a bad request.

POST/campaigns/:id/unready

Send a ready campaign back to draft so its settings, asset and recipients become editable again.

Success: 200 OK

Path parameters

FieldTypeDescription
idreqstring (uuid)Campaign id.

Response

json
200 OK

{ "data": { "id": "...", "status": "draft", ... } }

Errors

CodeHTTPWhen
CAMPAIGN_NOT_FOUND404Unknown id, not yours, or soft-deleted.
VALIDATION_ERROR409The campaign is not in ready status. The message names the status it is actually in.

POST/campaigns/:id/launch

Enqueue a render for every pending recipient and move the campaign to rendering. This is the call that spends quota.

Success: 200 OK

Launching is atomic against itself: a double-click or a retried request cannot enqueue every recipient twice. Renders are billed one at a time as they succeed, not charged up front, and a degraded render is never billed.

Path parameters

FieldTypeDescription
idreqstring (uuid)Campaign id.

Response

json
200 OK

{
  "data": { "id": "...", "status": "rendering", ... },
  "meta": { "jobsEnqueued": 42 }
}

Errors

CodeHTTPWhen
CAMPAIGN_NOT_FOUND404Unknown id, not yours, or soft-deleted.
ASSET_NOT_FOUND404The base video is gone — storage retention swept it. Upload it again.
CAMPAIGN_NOT_READY409Status is not ready, there are no pending recipients left (a concurrent launch won the race), or the campaign transitioned out of ready mid-call.
PLAN_LIMIT_REACHED402Plan quota is exhausted and the plan has no self-serve wallet rate (Enterprise). Contact your account manager.
WALLET_TOPUP_REQUIRED402Plan quota and wallet balance together do not cover the campaign. details carries shortfallVideos, estimatedShortfallCents and walletVideoRateCents.

POST/campaigns/:id/cancel

Stop a rendering campaign: aborts in-flight jobs, drops queued ones, and marks untouched recipients cancelled.

Success: 200 OK

Cancelled recipients were never charged — billing happens per successful render, so there is nothing to credit back.

Path parameters

FieldTypeDescription
idreqstring (uuid)Campaign id.

Response

json
200 OK

{
  "data": { "id": "...", "status": "cancelled", ... },
  "meta": { "cancelledRecipients": 12, "removedJobs": 9 }
}

Errors

CodeHTTPWhen
CAMPAIGN_NOT_FOUND404Unknown id, not yours, or soft-deleted.
CAMPAIGN_NOT_READY409Only a campaign in rendering status can be cancelled.

POST/campaigns/:id/retry-failed

Re-queue every failed, degraded, cancelled and orphaned-queued recipient in one call, at retry priority.

Success: 200 OK

Nothing to retry is a success, not an error: the call returns 200 with meta.jobsEnqueued: 0. Each retried recipient re-rolls its TTS take, so a bad-sounding greeting is redrawn rather than reproduced.

Path parameters

FieldTypeDescription
idreqstring (uuid)Campaign id.

Response

json
200 OK

{
  "data": { "id": "...", "status": "rendering", ... },
  "meta": { "jobsEnqueued": 3 }
}

Errors

CodeHTTPWhen
CAMPAIGN_NOT_FOUND404Unknown id, not yours, or soft-deleted.
CAMPAIGN_NOT_READY409Status must be completed, rendering, failed or cancelled.
ASSET_NOT_FOUND404The base video is gone — storage retention swept it.
PLAN_LIMIT_REACHED / WALLET_TOPUP_REQUIRED402Retries count against quota exactly like first renders.

POST/campaigns/:id/retry-recipient/:recipientId

Re-queue exactly one recipient, at retry priority.

Success: 200 OK

The source-drift refusal exists because the cloned voice is a function of the base recording: retrying against a replaced video would ship a recipient a video in a different voice from the rest of the campaign.

Path parameters

FieldTypeDescription
idreqstring (uuid)Campaign id.
recipientIdreqstring (uuid)Recipient id, which must belong to that campaign.

Query parameters

FieldTypeDescription
forcestringforce=true renders against the current base recording even when it changed since this recipient last rendered. Without it, that case is refused — see below.

Response

json
200 OK

{
  "data": { "id": "...", "status": "rendering", ... },
  "meta": { "jobsEnqueued": 1, "jobId": "render-..." }
}

Errors

CodeHTTPWhen
CAMPAIGN_NOT_FOUND404Unknown campaign id, not yours, or soft-deleted.
RECIPIENT_NOT_FOUND404The recipient does not belong to that campaign.
CONFLICT409The recipient is currently processing, or the base recording changed since its last render (retry with ?force=true, or re-render the whole campaign).
CAMPAIGN_NOT_READY409Status must be completed, rendering, failed or cancelled.
PLAN_LIMIT_REACHED / WALLET_TOPUP_REQUIRED402A retry costs one render.

POST/campaigns/:id/restore

Undo a soft-delete within the 7-day grace period.

Success: 200 OK

Restoring a campaign that is not deleted is a no-op success, not an error.

Path parameters

FieldTypeDescription
idreqstring (uuid)Campaign id.

Response

json
200 OK

{ "data": { "id": "...", "deletedAt": null, ... } }

Errors

CodeHTTPWhen
CAMPAIGN_NOT_FOUND404The row is gone for good — the retention sweep already purged it.

POST/campaigns/:id/clone

Copy a campaign's settings, base video and recipient list into a fresh draft. Recipients come back as pending; nothing is rendered.

Success: 201 Created

The base video is deep-copied into the new campaign, so deleting the source later does not break the clone. If any step fails the whole clone is rolled back — you never get a half-built campaign.

Path parameters

FieldTypeDescription
idreqstring (uuid)Source campaign id.

Body

The body is optional — send no body at all to accept both defaults.

FieldTypeDescription
namestring1-200 characters. Defaults to the source name plus " (copy)", truncated to fit.
descriptionstringDefaults to the source description.

Request

json
POST /campaigns/9a8b7c6d-.../clone
{ "name": "Q3 Outbound" }

Response

json
201 Created

{ "data": { "id": "<new id>", "name": "Q3 Outbound", "status": "draft", "recipientCount": 42, ... } }

Errors

CodeHTTPWhen
CAMPAIGN_NOT_FOUND404Unknown source id, not yours, or soft-deleted.
VALIDATION_ERROR400name outside 1-200 characters.