Skip to content
Reference

Get started

Overview

Avatars API

List, create, confirm, read and revoke an avatar.

Five customer routes and two the GPU box calls. The upload is deliberately two steps and the bytes never pass through this API.

Base: $LIVEGRID_API_URL, customer JWT, scoped to one organisation. Implemented in server/lg_avatar_routes.py. The flow these fit into is on Enrolling a face.

GET /public/avatars#

Everything not revoked, newest first.

{
  "avatars": [
    { "id": "av_...", "display_name": "Dana — support",
      "origin": "custom", "provider": "livegrid",
      "status": "ready", "status_reason": null,
      "source_image_url": "https://...signed...",
      "render_meta": {}, "created_at": "...", "ready_at": "...",
      "consent_ok": true }
  ],
  "count": 1
}

source_image_url is a signed URL, minted per response. The storage path is never returned — it is an internal address, and a signed URL is the only form a browser should hold.

consent_ok is computed live on every read, so a lapsed consent shows as lapsed the moment it lapses. Consent

POST /public/avatars#

Creates the row and hands back somewhere to put the recording.

{
  "display_name": "Dana — support",
  "content_type": "video/mp4",
  "client_ref": "acct_9931",
  "consent": {
    "subject_name": "Dana Okafor",
    "subject_email": "dana@example.com",
    "attestation": "Dana recorded this on 2026-09-08 and agreed to its use on customer calls."
  }
}

display_name, consent.subject_name and consent.attestation are required. Missing consent is a 400, not a default.

Accepted content_type: video/mp4, video/webm, video/quicktime, image/jpeg, image/png, image/webp. Anything else is a 400 that lists what is accepted.

{
  "avatar_id": "av_...",
  "upload_url": "https://...signed...",
  "method": "PUT",
  "headers": { "Content-Type": "video/mp4" },
  "max_bytes": 1073741824,
  "accepted_types": ["video/mp4", "..."],
  "source_kind": "video"
}

max_bytes is 1 GB for video and 12 MB for an image. PUT the file to upload_url yourself — the bytes never pass through the API.

POST /public/avatars/<id>/uploaded#

Confirms the object is really in storage. The API asks storage rather than believing the client.

  • 200 {"avatar_id": "...", "status": "training"}
  • 409 the object is not there yet — {"error": "...", "hint": "the upload did not complete — PUT the file, then confirm again"}. Recoverable: retry the PUT with the same ticket.
  • 404 no such avatar in this organisation.

GET /public/avatars/<id>#

One avatar, same shape as a row from the list. 404 if it is not yours.

DELETE /public/avatars/<id>#

QueryDefault
reasonwithdrawn by customer

Marks the avatar revoked, marks the consent revoked, and deletes the stored recording and the artifact. The row itself stayslg_calls.avatar_id references it, and erasing the history of every call a person appeared in would be an odd way to honour a request to be forgotten.

Internal routes#

POST /internal/avatars/claim and POST /internal/avatars/<id>/enrolled are the GPU box's side of the fence, authenticated with LIVEGRID_RENDER_SECRET rather than a customer token. They are not part of the customer API and are documented here only so that a 401 on them is recognisable.

With the secret unset both answer 503, not 401: an enrolment queue anyone can drain is worse than one that does not run, because a claim marks a row in progress and an unauthenticated caller could quietly stall every avatar in the system without ever producing an artifact.

Up nextWebhook and ingest API