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>#
| Query | Default |
|---|---|
reason | withdrawn by customer |
Marks the avatar revoked, marks the consent revoked, and deletes the stored
recording and the artifact. The row itself stays — lg_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.