---
name: img2avatar
description: Generate rigged 3D avatars (VRM + Unreal GLB) from a text prompt, a photo, or an image — headless, via one API. text/photo → A-pose views → mesh + rig → VRM. Optional facial expressions (talk/blink/eye-track).
compatibility: Requires curl and network access. Works on Claude.ai, Claude Code, and the API.
homepage: https://img2avatar.com
metadata: {"version":"0.5.2","updated":"2026-07-03","author":"img2avatar","openclaw":{"requires":{"bins":["curl","jq"]},"primaryEnv":"AVATAR_API_KEY","emoji":"bust_in_silhouette","homepage":"https://img2avatar.com"}}
---

# img2avatar — 3D avatars for AI agents

Generate a fully **rigged 3D avatar** (`.vrm` for VRM apps + `.glb` for Unreal / game engines) from a **text prompt, a photo, or an image**. The whole pipeline runs server-side and headless: image generation (fal) → 3D mesh + rigging (Meshy) → VRM / Unreal conversion. One avatar takes **~4–8 minutes**.

> **Skill version 0.5.2 (2026-07-03).** To check for updates, fetch `https://img2avatar.com/skill.md` and compare the `updated` date in the frontmatter. If newer, run `npx skills add img2avatar.com`. New in 0.5.x: **facial expressions** (talk / blink / eye-tracking), generated automatically + free for every avatar — see [Facial expressions & animation](#facial-expressions--animation).

## Installation

- **Claude Code**: `npx skills add img2avatar.com`, or copy this file to `.claude/skills/img2avatar/SKILL.md`.
- **Any agent**: the API is self-describing — OpenAPI 3.1 at `https://api.img2avatar.com/openapi.json` (public, no key) and a browsable UI at `https://api.img2avatar.com/docs`.

## Setup

- **Base URL**: `https://api.img2avatar.com`
- **Auth** (every route except `/openapi.json`, `/docs`, and `/auth/register`): send the key either way —
  - header `Authorization: Bearer $AVATAR_API_KEY`, or
  - query `?token=$AVATAR_API_KEY` (handy for SSE / browser `<img>`/`<model>` tags).

### Getting a key (self-serve — no signup, no card)

If you don't already have a key, mint one. It comes with a free quota (avatars are free for now):

```bash
AVATAR_API_KEY=$(curl -s -X POST https://api.img2avatar.com/auth/register \
  -H "Content-Type: application/json" -d '{"label":"my-agent"}' | jq -r .apiKey)
# Store it (env / secret). It's shown only once. Check your quota anytime:
curl -s https://api.img2avatar.com/auth/me -H "Authorization: Bearer $AVATAR_API_KEY"
# {"unlimited":false,"quotaUsed":0,"quotaLimit":25,"quotaRemaining":25}
```

### Can your HTTP tool only do GET? (no POST)

Some agent runtimes can only issue GET requests. The whole flow works over GET too — mint a key and create a job with plain GET URLs:

```bash
# 1. Mint a key (GET)
AVATAR_API_KEY=$(curl -s "https://api.img2avatar.com/auth/register?label=my-agent" | jq -r .apiKey)
# 2. Create a job (GET) → {"jobId":"...","viewerUrl":"https://img2avatar.com/g/..."}
curl -s "https://api.img2avatar.com/jobs/new?token=$AVATAR_API_KEY&prompt=a%20cyberpunk%20samurai&style=realistic"
# 3. Poll GET /jobs/{id} for status (see the core flow). Download via GET as usual.
```

`GET /jobs/new` accepts `prompt`/`mode`/`imageUrl`/`style`/`poseMode`/`imageModel`/`callbackUrl` + `token`. It **creates a new job on every call** (it spends quota), so **don't share the `/jobs/new` URL** — share the returned `viewerUrl` (that one is safe + idempotent).

## Critical rules

- **Pre-flight: confirm your key before you promise anything.** Run the verify call below; if it's `401`, you have **no usable key** — register one (above) or tell the user to set `AVATAR_API_KEY`. **Never claim a generation started unless you got back a `jobId`.** Don't say "I'm generating, I'll send the link" before a real create call (`POST /jobs`, or `GET /jobs/new`) returned one.
  ```bash
  curl -s https://api.img2avatar.com/ -H "Authorization: Bearer $AVATAR_API_KEY"
  # {"status":"ok","service":"avatar-forge-api"}  ← 401 means no/invalid key
  ```
- **Every generation costs real money** (fal image gen + Meshy mesh/rig) and **spends one unit of your free quota**. Never generate speculatively or in bulk — only when the user asks, one at a time unless told otherwise. At `402` your free quota is used up (billing is coming; ask the operator to raise it).
- A job takes **~4–8 minutes**. Poll or stream; never assume it's instant or block the user without telling them.
- `prompt` mode renders a character from text; `photo` / `image` modes turn a real person/picture into an avatar.
- Output is **A-pose → forced T-pose**, **humanoid-rigged** (VRM humanoid bones + Mixamo-named bones for Unreal). The subject must be a **biped** — a human or an upright/anthropomorphic character. Animals & non-humanoid subjects can fail at the `rig` step (`Meshy ... Pose estimation failed`); for a creature, prompt an upright two-legged stance, e.g. "an owl-headed robot **standing upright on two legs, arms at its sides, humanoid stance**".

## The core flow (text → avatar)

```bash
KEY=$AVATAR_API_KEY
# 1. Create a job → returns {"jobId":"...","viewerUrl":"https://img2avatar.com/g/..."}
RESP=$(curl -s -X POST https://api.img2avatar.com/jobs \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"mode":"prompt","prompt":"a cyberpunk samurai","style":"realistic"}')
JOB=$(echo "$RESP" | jq -r .jobId)
echo "$RESP" | jq -r .viewerUrl   # ← shareable page; shows live progress + the result

# 2. Poll until terminal (pending → running → done | error)
while :; do
  S=$(curl -s -H "Authorization: Bearer $KEY" https://api.img2avatar.com/jobs/$JOB | jq -r .status)
  echo "$S"; { [ "$S" = done ] || [ "$S" = error ]; } && break; sleep 15
done

# 3. Download the VRM
curl -s -H "Authorization: Bearer $KEY" \
  https://api.img2avatar.com/jobs/$JOB/assets/vrm-hd -o avatar.vrm
```

## Getting the result — pick the pattern that fits your agent

A job takes ~4–8 min, so how you collect it depends on what kind of agent you are:

- **You just need to show the user an avatar** → don't wait. `POST /jobs` returns a **`viewerUrl`** (`https://img2avatar.com/g/{id}`); hand that to the user. The page shows live progress and the finished 3D avatar. Zero polling.
- **You need the file in this session** → **poll** `GET /jobs/{id}` until `done` (above), or — if your runtime can schedule — fire the job, store the `jobId`, end the turn, and re-check later (the `jobId` is durable). Or stream `GET /jobs/{id}/stream` (SSE) for live progress.
- **You're a backend / automation with a public HTTPS endpoint** → pass **`callbackUrl`** and get **pushed** the result; no polling. See [Recipes](https://img2avatar.com/references/recipes.md#webhook) + [API reference](https://img2avatar.com/references/api.md).

```bash
# Webhook: POST the result to your endpoint when done (instead of polling)
curl -s -X POST https://api.img2avatar.com/jobs \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"mode":"prompt","prompt":"a cyberpunk samurai","callbackUrl":"https://your-app.com/hooks/img2avatar"}'
# → your endpoint receives a signed POST { jobId, status, viewerUrl, result, error }.
# The X-Img2Avatar-Event header carries the status; status is one of:
#   done | error  (VRM pipeline) → then face_done | face_error  (auto-chained face step).
# For the FULLY-ready avatar (VRM + facial expressions) wait for face_done.
```

> **Verifying the webhook.** `X-Img2Avatar-Signature` is `"sha256=" + HMAC_SHA256(rawBody, KEY)` where **KEY is the server operator's master key — NOT your self-serve key**, so a self-serve key holder **cannot** verify the signature. Hosted self-serve consumers should rely on **HTTPS** + confirming the `jobId` with `GET /jobs/{id}` before trusting the payload. Only a **self-hosted operator** (who holds the master key) can verify the HMAC.

## Inputs & options (`POST /jobs`)

| field | values |
|---|---|
| `mode` | `prompt` (text) · `photo` (+ `photoDataUrl`) · `image` (+ `imageUrl`) |
| `imageUrl` | **public, direct image link** for `mode=image` — the server downloads it. Must be a real image file (`.jpg`/`.png`/`.webp`), **not** a web page or a login-gated/expiring link (Telegram `t.me` userpics, Discord/WhatsApp CDN tokens, Google Drive view pages all fail). A bad URL is rejected at create with `400`. For a private photo, host it publicly or use `mode=photo` with `photoDataUrl`. |
| `prompt` | text description (prompt mode) |
| `style` | `realistic` `cartoon` `anime` `fortnite` `ps1` `low-poly` `voxel` `claymation` |
| `imageModel` | `nano-banana` (default, fast) · `gpt-image-2` (more detailed, slower; its upstream provider occasionally returns `Forbidden`/rate-limit — retry once or fall back to `nano-banana`) |
| `poseMode` | `a-pose` (default) · `t-pose` · `rest` |

## Assets (`GET /jobs/{id}/assets/{name}`)

`vrm` (optimized VRM) · `vrm-hd` (full-quality VRM) · `unreal.glb` (UE5 / Mixamo bones) · `mesh.glb` · `rigged.glb`. **`GET /jobs/{id}/result` is the authoritative asset set** — call it to see which are available + warnings + bone mapping. The `face-*` assets (manifest / atlases / data) appear there once the facial expressions are ready.

## Facial expressions & animation

The generated `.vrm` has a body rig but **no face morph targets**. **Facial-expression
atlases — that make the avatar talk (lip-sync), blink, show emotions, and track with its
eyes** — are generated **automatically and for free** for every completed avatar (the face
step is auto-chained right after the VRM is `done`, at no quota charge). Wait for the
`face_done` webhook status (or poll `GET /jobs/{id}/face`) for the fully-ready avatar.

`POST /jobs/{id}/face` is only needed to **regenerate** the expressions (`?force=true`,
which charges **one quota unit**) or to **re-run a failed face step**:

```bash
# Regenerate / re-run the face step (the auto-chained run already happened for free).
#    Idempotent — returns "running" if already generating/generated, unless ?force=true.
#    ?force=true regenerates and charges one quota unit.
curl -s -X POST -H "Authorization: Bearer $KEY" https://api.img2avatar.com/jobs/$JOB/face
# → { "jobId":"…", "status":"running", "message":"Face expression generation started" }
#    Options: ?force=true (regenerate, costs one quota unit) · ?model=nano-banana-pro|gpt-image-2 · ?visemes=false (skip mouth shapes)

# 2. Poll the face step until terminal (idle → running → done | error). Takes a few minutes.
curl -s -H "Authorization: Bearer $KEY" https://api.img2avatar.com/jobs/$JOB/face
# → { "status":"done" }   (or "running" / "error" with an "error" message)

# 3. The atlases are now served as job assets:
#    face-manifest.json  — expression/viseme name → atlas filename
#    face-atlas-*.png     — one texture per expression / viseme / blink frame
#    face-data.json       — eye geometry (iris/sclera color, eyelid openings) for synthetic eyes
curl -s -H "Authorization: Bearer $KEY" https://api.img2avatar.com/jobs/$JOB/assets/face-manifest.json
```

- **`POST /jobs/{id}/face`** → `400` if the VRM isn't ready yet, `402` if a `?force=true`
  regenerate exceeds your free quota. It's safe to call repeatedly (idempotent unless `?force=true`).
- **`GET /jobs/{id}/face`** → `{ status: "idle"|"running"|"done"|"error", error?, updatedAt? }`.

### Animating it in an app

The expression atlases are driven by the **`avatar-forge-face`** package (Three.js), which
makes our avatars behave like a `@pixiv/three-vrm` VRM — so any app drives them with the
standard interface (`vrm.expressionManager.setValue('happy'|'aa'|'blink', w)`,
`vrm.lookAt.target = obj`, `vrm.update(dt)`):

```ts
import { installKamiFace, fetchKamiFaceAssets } from 'avatar-forge-face/runtime'
const vrm = await myVrmLoader.loadAsync(vrmUrl)
const assets = await fetchKamiFaceAssets(jobId, { apiBase: 'https://api.img2avatar.com', token: KEY })
await installKamiFace(vrm, assets)        // now drive vrm.expressionManager / lookAt / update as usual
```

**React / R3F hosts** should use the **`@kami/avatar-face-react`** package instead of
wiring `installKamiFace` by hand — it ships an `<AvatarFace>` component and a `useAvatarFace`
hook. The low-level `installKamiFace` façade above is for **vanilla three.js**.

Full client integration guide (lip-sync, gaze, blink, asset shapes, the low-level
controller) lives in the package README: `avatar-forge-face`. The package is internal to
the kami monorepo today (not yet on npm); the atlases themselves are public job assets you
can consume from any engine.

## Read back a job's inputs (prompt / style / params)

`GET /jobs/{id}/assets/config.json` returns exactly what the job was created with — handy to show the user, or to re-run with the same settings:

```bash
curl -s -H "Authorization: Bearer $KEY" https://api.img2avatar.com/jobs/$JOB/assets/config.json
# { "mode":"prompt", "prompt":"a cyberpunk samurai", "style":"realistic",
#   "imageModel":"nano-banana", "poseMode":"a-pose" }   ← only the fields that were set
```

(`config.json` = the **inputs**, mirroring `GET /jobs/{id}/result` = the **outputs**. `GET /jobs/{id}` carries only runtime state: status / step / progress / error.)

## View / share an avatar in the browser

Every job has a **shareable page** — give the user (or post) the job URL:

```
https://img2avatar.com/g/{JOB_ID}
```

It renders the 3D avatar, produces a **rich link preview** (the character image as `og:image`, the prompt as the title), updates live if the job is still generating, and has a "Create your own" call-to-action. It loads the model through a server-side proxy, so the link is public and exposes no API key.

(Lower-level: `https://img2avatar.com/ar?url=<url-encoded proxied asset url>` renders any model URL directly, if you only have an asset URL.)

## Troubleshooting

- **Job goes `error` right after the image, with `Biped pre-check: no full-body character detected`** — the pre-3D gate caught a non-biped BEFORE spending 3D credits (cheaper + faster than the old rig-step death). Same fix: re-run with a prompt/photo that shows a full-body upright humanoid, head to toe (see Critical rules). Stylized presets (ps1/voxel/pixel) skip this gate.
- **Job goes `error` at the `rig` step (`Pose estimation failed`)** — the subject isn't a recognizable biped (stylized subjects reach this step because the pre-check can't judge them). Re-run with a prompt that puts it upright on two legs in a humanoid stance (see Critical rules). Cost is per-attempt, so fix the prompt before retrying.
- **`POST /jobs` returns `Forbidden` (or the job errors immediately) with `imageModel: gpt-image-2`** — a transient upstream-provider error, not a permanent block. Retry once, or use `nano-banana`.
- **`400` on create with `mode=image`** — the `imageUrl` isn't a downloadable image (dead link, web page, or a login-gated/expiring URL like a Telegram `t.me` userpic). Use a public, direct image-file URL, or switch to `mode=photo` with `photoDataUrl`.
- **`503` on create** — the job queue is full; wait and retry.
- **Job stuck `running`?** — it shouldn't; a finished job flips to `done`. If `GET /jobs/{id}/result` returns `200` but status is still `running`, the assets are ready anyway — fetch them.

## More (load only what you need)

- **[Recipes](https://img2avatar.com/references/recipes.md)** — photo→avatar, image-URL→avatar, SSE streaming, shareable viewer link, picking the model, restart-from-step, batch, cancel.
- **[API reference](https://img2avatar.com/references/api.md)** — every endpoint, the job lifecycle, full request/response schemas, SSE event shape, errors.
- **OpenAPI**: `https://api.img2avatar.com/openapi.json` · **Docs UI**: `https://api.img2avatar.com/docs`
