# FaceNinja API: compact notes for agents

Base URL `https://faceninja.net` (or `$FACENINJA_API_BASE`). Every call except the device sign-in sends
`Authorization: Bearer fn_live_...`. JSON in, JSON out. The full reference is https://faceninja.net/docs/ and the
OpenAPI 3.1 file https://faceninja.net/docs/openapi.yaml. `scripts/faceninja.py` wraps all of this; read on only for
what it does not cover.

## Workflow

```
POST /v1/photos?filename=me.jpg   body = the image bytes, Content-Type: image/jpeg   -> 201 {"id": "ph_..."}
POST /v1/heads                    {"photo_ids": ["ph_..."], "name": "Ana"}            -> 202 {"id": "hd_...", "status": "generating"}
                                  header Idempotency-Key: <uuid>  (a retry returns the same head, no second charge)
GET  /v1/heads/{id}?wait=60       holds up to 60 s; loop until status is completed | failed | deleted
POST /v1/heads/{id}/files         {"format": "glb"}  -> {"url", "filename", "bytes", "sha256"}; the url works for 60 s
GET  <url>                        download, without the API key
```

Statuses: `generating` (reference portrait in the cloud) → `queued` (waiting for a builder; `queue.position`, `queue.eta_s`)
→ `processing` (`stage`, `progress` 0–100) → `completed` or `failed` (`error.code`, `error.message`; the credits are
returned). `DELETE /v1/heads/{id}` before it finishes also returns the credits.

Formats for `files`: `glb`, `fbx` (both with `arkit: true|false`), `obj`, `stl`, `ply`, `metahuman_zip`. Optional download
options: `texture` (jpg|png|none), `normal_map` (opengl|directx), `texture_size` (4k|2k|1k), `cornea`, `scale`; a new
option set answers `202` while it is prepared once (pass `wait` up to 25).

## Money

`GET /v1/account` → `credits`, `held_credits`, `prices.head` (1500), `prices.credits_per_usd` (100). A head's price is held
when it is created, kept when it completes, returned when it fails. `POST /v1/heads?dry_run=true` validates and quotes
without creating anything. `402 insufficient_credits` means nothing was created. Credits are bought in Factory only.

## Errors and limits

`{"error": {"code", "message", "field"?, "hint"?, "request_id"}}`; branch on `code`. 600 requests a minute per account,
30 head creations a minute, 20 file links a minute per head; `429` carries `Retry-After`. Photos: JPEG, PNG, WebP,
HEIC/HEIF, AVIF, 1 KB to 25 MiB, 1 to 15 per head, one person, unused photos expire after 24 h. Finished heads keep
their files for 30 days (`410 files_expired` after).

## Device sign-in (how `faceninja.py login` gets a key)

```
POST /v1/device/code   {"client_name": "Claude Code on host", "client": {"agent", "host", "os", "version"}}
  -> {"device_code", "user_code": "WXYZ-2345", "verification_uri", "verification_uri_complete", "expires_in": 600, "interval": 5}
POST /v1/device/token  {"device_code"}   every `interval` seconds
  -> 400 authorization_pending | slow_down (add 5 s to the interval) | expired_token | access_denied | invalid_grant
  -> 200 {"access_token": "fn_live_...", "key_id", "key_prefix", "key_name", "email"}   (once; store it, never print it)
DELETE /v1/keys/current   revokes the key that sends it
```

The person opens `verification_uri_complete`, signs in to FaceNinja and clicks Approve or Deny. A code works once and
expires after 10 minutes.

## Endpoints

<!-- generated from public/docs/openapi.yaml by tools/skill/build.mjs: do not edit by hand -->
| Method | Path | What it does |
| --- | --- | --- |
| `GET` | `/v1/account` | Read the account |
| `POST` | `/v1/photos` | Upload one photo |
| `POST` | `/v1/photos/uploads` | Get direct upload links |
| `GET` | `/v1/photos/{id}` | Read a photo |
| `DELETE` | `/v1/photos/{id}` | Delete an unused photo |
| `GET` | `/v1/heads` | List heads |
| `POST` | `/v1/heads` | Create a head |
| `GET` | `/v1/heads/{id}` | Read a head, optionally waiting |
| `DELETE` | `/v1/heads/{id}` | Delete a head |
| `GET` | `/v1/heads/{id}/events` | Follow a head (Server-Sent Events) |
| `GET` | `/v1/heads/{id}/files/{kind}` | Get a download link |
| `POST` | `/v1/heads/{id}/files` | Get a download link (body form) |
| `GET` | `/v1/webhooks` | List webhook endpoints and recent deliveries |
| `POST` | `/v1/webhooks` | Add a webhook endpoint |
| `DELETE` | `/v1/webhooks/{id}` | Remove a webhook endpoint |
| `POST` | `/v1/device/code` | Start a device sign-in |
| `POST` | `/v1/device/token` | Collect the key of an approved sign-in |
| `DELETE` | `/v1/keys/current` | Revoke the key that sends this request |
<!-- endpoints:end -->
