> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Avatar video render lifecycle

> How an async avatar-video render job moves from queued to completed — script + merge-field inputs, the completion contract (clip bytes or a pre-computed digest), provenance handoff, idempotent PATCH transitions, and deletion semantics.

# Avatar video render lifecycle

An avatar persona can join a live room, but it can also render a
**personalized video clip** from a text script — the asynchronous
counterpart to the live join. That render is modeled as a pollable job:
`POST /api/v1/video/avatar-video-renders` script it, your render runtime
performs the render off-platform, and `PATCH` transitions report the
result back. This page is the model behind that loop. Request shapes live
in the [video API reference](/api-reference/endpoints/video); the
[avatar bot dispatch queue](/concepts/avatar-bot-dispatch-queue) page
covers the live-join path.

## The render-job state machine

A render job (`vavr_…` id) has four statuses:

1. **queued** — created with the merged script, waiting for your render
   runtime to pick it up.
2. **rendering** — your runtime has started the render.
3. **completed** — terminal. Carries `output_url` plus the content
   credential minted at completion.
4. **failed** — terminal. Carries a human-readable `error`.

Allowed transitions are `queued → rendering`,
`queued/rendering → completed`, and `queued/rendering → failed`. Both
terminal states reject further transitions with a
`409 VIDEO_AVATAR_RENDER_INVALID_TRANSITION`, so each job is
**append-only**: a terminal verdict can never be overwritten while the
job still exists (see deletion below). A `completed` report without an
`output_url`, or without its provenance credential, is rejected outright.

## Authoring inputs

You create a job with four authoring inputs:

* **`avatar_agent_id`** — the persona to render from (`vaa_…` id, created
  in **Video → Avatar agents**). The persona must be enabled; a disabled
  persona rejects the job with `400 VIDEO_AVATAR_RENDER_INVALID`.
* **`script_template`** — the text the avatar speaks (up to 5,000
  characters), with `{{merge_field}}` tokens.
* **`locale`** — optional TTS language override (up to 35 characters);
  without it, the persona's own locale applies.
* **`merge_fields`** — token substitutions, where each key is a bare
  token name like `first_name` (not `{{first_name}}`).

**Unresolved tokens are rejected up front.** A rendered clip is a single
deterministic render — unlike a mass campaign send, there is no
per-recipient fallback that could fill a blank value later, so every
`{{token}}` you write must have a supplied value at script time. A
missing value returns `400` naming the missing tokens, before any render
work is queued.

## Completion contract

When your render runtime has the clip, it reports `completed` over
`PATCH /api/v1/video/avatar-video-renders/:id`. The report must bind the
clip in one of two ways:

* **`video_base64`** — the raw clip bytes. Orbit decodes them and
  computes the SHA-256 digest **server-side**, so an empty or
  mislabeled payload is caught before the credential is minted.
* **`video_sha256` + `video_bytes`** — a pre-computed digest and byte
  count, for when the clip is too large to attach.

Orbit never calls the provider render API (Tavus, HeyGen, D-ID, …)
directly and stores no provider credentials — your own render runtime,
holding its own credentials, performs the render and this PATCH is its
callback. That separation is what lets a completion report arrive without
embeddable clip bytes and still prove which clip it refers to.

## Provenance handoff

Completing a job mints a **content credential** — a tamper-evident,
signed provenance manifest — and persists it on the job as
`content_credential`, next to the `output_url`. The manifest binds the
clip bytes (`video_sha256`), the exact rendered script
(`script_sha256`), the persona, the tenant, and the render provider, so
the credential travels with the clip when you attach the URL to a
campaign send or a 1:1 message. A `completed` transition that would skip
that mint is rejected — every completed clip must carry the manifest,
never just a subset.

Verification (`POST /api/v1/video/avatar-video-renders/verify`) is
stateless and read-scoped: pass the token to check its signature, and
optionally pass `video_base64` or `video_sha256` to confirm it is still
bound to the clip in hand (`video_mismatch` otherwise). The full marking
model is in [synthetic-media
provenance](/concepts/synthetic-media-provenance).

## Limits

All limits are enforced at script or completion time, so a failed request
never consumes render capacity:

* Script template: **5,000** characters.
* Merge fields: **20** keys; key **≤ 64** characters; value **≤ 500**
  characters each.
* Locale: **35** characters.
* Output URL and error message: **2,048** / **500** characters.
* Per-tenant job history: **200** jobs — the library is bounded, so
  create #201 rejects until you delete old jobs.

## Idempotency and PATCH callbacks

PATCH transitions are safe to retry within the same terminal verdict.
The state machine collapses a duplicate callback onto the same outcome:
once `completed` or `failed` is recorded, any further PATCH is a `409`,
and the first answer stands. A `rendering` report arriving before the
completion report is accepted but is not required — `queued → completed`
is a legal direct transition. This is what makes the completion callback
retry-safe without an idempotency key: the terminal-state gate is the
dedupe.

## Relationship to the dispatch queue

These two concepts split the surface cleanly:

* **[Avatar bot dispatch queue](/concepts/avatar-bot-dispatch-queue)** —
  the **live join**: dispatching a persona into a real-time room with
  roster verification.
* **This page** — the **async render**: script → clip that outlives any
  room.

Both resolve personas from the same registry, and both follow the
non-secret-reference design: Orbit persists the request and a plain
handle, your integration runtime holds provider credentials and performs
the actual work.

## Tenant audit and deletion

Every transition — create, status update, delete — is written to the
tenant audit ledger with the persona id, status, and whether an output
and credential are present. `DELETE /api/v1/video/avatar-video-renders/:id`
removes a job from the rendering history free-form (terminal or not); the
audit entry persists past the job itself. Deletion is how you keep the
bounded 200-job history from rejecting new renders, and it lets your
compliance retention policy clear rendered-script text without touching
live traffic.

## Scope note

The render surface produces a video asset only. Attaching its `output_url`
to a campaign or message send always exits through the Devotel
softswitch-backed send path; no outbound (MT) voice or SMS leg is ever
originated here.

## Related

<CardGroup cols={2}>
  <Card title="Synthetic-media provenance" href="/concepts/synthetic-media-provenance">
    The full content-credential model the completion handoff mints.
  </Card>

  <Card title="Avatar bot dispatch queue" href="/concepts/avatar-bot-dispatch-queue">
    The live-join counterpart: persona-to-room dispatches and roster verification.
  </Card>

  <Card title="AI video avatar agents — guide" href="/guides/ai-video-avatar-agents">
    Persona registry, join plans, and render jobs end to end.
  </Card>

  <Card title="Audit ledger model" href="/concepts/audit-ledger-model">
    Where every transition's audit entry lands and how to query it.
  </Card>
</CardGroup>
