Developers
KeenTools Cloud API reference
Every endpoint, parameter, and response of the KeenTools Cloud API, as plain text.
Interactive referenceMarkdownOpenAPI spec
KeenTools Cloud API for 3D head reconstruction.
Quick Start
- Initialize: Send POST
/v1/avatar/initwith the number of photos (2-15). You'll receive an avatar ID and pre-signed upload URLs. - Upload: Upload each photo directly to its pre-signed URL using HTTP PUT with the raw image bytes.
- Process: Start reconstruction with POST
/v1/avatar/{id}/process. - Monitor: Poll GET
/v1/avatar/{id}/get-statusuntilstatus: completed. - Download: Fetch your 3D model via GET
/v1/avatar/{id}/get-3d-model.
Authentication
Include your API key as a Bearer token in the Authorization header:
Authorization: Bearer ak_live_...
Obtain an API key at https://cloud.keentools.io (Settings > API Keys).
Legacy *.keentools.workers.dev hosts remain supported for existing integrations; new integrations should use the server URL above.
Credits
Credit-bearing endpoints place a temporary credit hold before processing starts:
POST /v1/avatar/{id}/processholds generation credits when the request is accepted (2xx). Credits are captured after the session completes successfully, or released if processing fails or the session is deleted.GET /v1/avatar/{id}/get-3d-modelcreates a new charge for every response withevent: redirect, including repeated calls with identical parameters.retry-afterand non-2xx responses release the hold.
If a credit-bearing request is rejected outright (non-2xx), the hold is released immediately.
Check your balance and top up at https://cloud.keentools.io. See https://cloud.keentools.io for current pricing.
Base URL
All endpoints below are relative to https://api.keentools.io.
POST /v1/avatar/init
Creates a new avatar job for a given number of photos. Returns avatar id and pre-signed AWS S3 PUT URLs for direct photo upload.
To upload photos, send an HTTP PUT request to each returned URL with the raw image bytes as the request body and the appropriate Content-Type header (e.g., image/jpeg or image/png).
Request body
image_count(integer, required) — The number of photos to use for reconstruction (2–15)
Responses
- 200 — Avatar initialized
avatar_id(string, required) — Avatar IDimg_urls(array of string, required) — Pre-signed AWS S3 PUT URLs for direct photo upload. Upload each photo by sending an HTTP PUT request to the corresponding URL with: The raw image bytes as the request body; Content-Type header set to the image MIME type (e.g., image/jpeg, image/png)
- 400 — Too many photos. Reduce number to max 15 / Too few photos. Provide at least 2
- 401 — Missing, malformed, or invalid API key
- 500 — Unexpected internal error
- 502 — Cloud infrastructure unexpected error. Please try again later
POST /v1/avatar/{avatar_id}/process
Billed request: This request is billed. See pricing details.
Start avatar reconstruction. Call this after uploading all photos to the pre-signed URLs from /init.
This endpoint charges generation only; model downloads are charged separately by /get-3d-model.
Returns 400 if reconstruction is already in progress for this avatar.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
avatar_id |
path | yes | string | Avatar ID. |
Request body
expressions_enabled(boolean, optional) — Create avatar with facial expressions. When true, the "expression" blendshape group becomes available in /get-3d-model.focal_length_type(FocalLengthType, required) — Select how the 35 mm equivalent focal length should be handled for reconstruction
Responses
- 200 — Avatar reconstruction started
- 400 — Malformed Focal Length data: the quantity of focal length values does not match the quantity of photos / Reconstruction already in progress
- 401 — Missing, malformed, or invalid API key
- 402 — Insufficient credits
- 404 — Avatar not found
- 410 — The requested avatar is no longer supported because the API was upgraded. Please initialise a new avatar
- 500 — Internal server error (e.g. credit hold infrastructure failure)
- 502 — Cloud infrastructure unexpected error. Please try again later
GET /v1/avatar/{avatar_id}/get-status
Check reconstruction status. Poll this endpoint after calling /process.
Returns one of five statuses: not_started, running (with progress 0.0–1.0), completed, failed (with error message), or deleted.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
avatar_id |
path | yes | string | Avatar ID. |
Responses
- 200 — Avatar reconstruction status
- NotStartedResponse — The reconstruction has not started yet
status(string (not_started), required)
- RunningResponse — The reconstruction is in progress
data(object, required) — The reconstruction is in progressdata.progress(number, required) — Reconstruction progress (0 = just started, 0.5 = 50%, 1 = completed)
status(string (running), required)
- FailedResponse — Reconstruction failed
data(object, required) — Reconstruction faileddata.error_message(string, required) — Error message
status(string (failed), required)
- CompletedResponse — Reconstruction finished successfully
status(string (completed), required)
- DeletedResponse — Session was deleted
status(string (deleted), required)
- NotStartedResponse — The reconstruction has not started yet
- 401 — Missing, malformed, or invalid API key
- 404 — No avatar found
- 502 — Cloud infrastructure encountered an unexpected error. Please try again later
GET /v1/avatar/{avatar_id}/get-3d-model
⚠️ EVERY response with
event: redirectcreates a new charge.Calling this endpoint again, even with the same avatar and identical parameters, charges your account again. Stop polling immediately after the first
redirectresponse and downloaddata.urldirectly. Responses withevent: retry-afterand non-2xx responses are not charged.
See pricing details.
Generates and returns head mesh data.
This endpoint does NOT return the mesh directly. Instead, it uses a polling protocol:
- If the mesh is still being generated, returns
{ "event": "retry-after", "data": { "time_sec": N } }. Wait N seconds, then call again. - When the mesh is ready, returns
{ "event": "redirect", "data": { "url": "https://..." } }. Stop polling and download the mesh from that pre-signed URL.
Correct polling
for (;;) {
const response = await fetch(modelEndpoint, {
headers: { Authorization: "Bearer " + apiKey },
});
const result = await response.json();
if (result.event === "retry-after") {
await delay(result.data.time_sec * 1000);
continue;
}
// This redirect response is billed. Stop polling now.
await download(result.data.url);
break;
}
Incorrect: continues charging after the model is ready
// Never poll on a fixed interval without stopping after redirect.
setInterval(() => fetch(modelEndpoint), 5000);
OBJ format note: Returns a ZIP archive (.obj + .mtl + texture files).
GLB format note: The GLB file contains two meshes - one textured mesh with 4 primitives (Head, EyeLeft, EyeRight, Teeth) each with its own texture, and one wireframe mesh with 4 line primitives. All textures are JPEG/PNG, up to 4096x4096 each.
Blendshape support: GLB supports expression and arkit blendshapes. OBJ supports expression blendshapes only.
Blendshapes serialization: The blendshapes array parameter must be serialized as a single comma-separated value (e.g., ?blendshapes=arkit,expression). Using repeated query parameters will result in a "duplicate field" error.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
avatar_id |
path | yes | string | Avatar ID. |
mesh_format |
query | no | string (obj, glb) | Mesh format. glb: Single binary file with embedded textures, morph targets, and wireframe edges. Recommended for most use cases. obj: Wavefront OBJ. Returns a ZIP archive (.obj + .mtl + texture files). Default: obj. |
mesh_lod |
query | no | string (high_poly) | Level of detail. Default: high_poly. |
blendshapes |
query | no | array of string (expression, arkit) | Blendshape groups to include in the output mesh. Serialized as comma-separated values, NOT repeated params. expression: Numbered expression morphs (Expression 01, Expression 02, ...). Available for GLB and OBJ when expressions_enabled=true was set during processing. arkit: 51 ARKit-compatible morph targets (browDownLeft, eyeBlinkLeft, mouthSmileRight, etc.). GLB only. Format support:; GLB: Supports both expression and arkit blendshapes. OBJ: Supports expression blendshapes only. |
texture |
query | no | null | string (jpg, png) | Add texture. |
edges |
query | no | boolean | Add wireframe edge lines to the mesh (GLB only) Default: false. |
Responses
- 200 — Check the "event" field in the response:
retry-after: Model is still being generated. Waitdata.time_secseconds and retry.redirect: Model is ready. Download from the pre-signed URL indata.url.- RetryAfter — The mesh is still being generated. Wait the specified number of seconds, then call the endpoint again. Typical generation takes 10–60 seconds depending on mesh options.
data(object, required) — The mesh is still being generated. Wait the specified number of seconds, then call the endpoint again. Typical generation takes 10–60 seconds depending on mesh options.data.time_sec(integer, required) — Number of seconds to wait before retrying
event(string (retry-after), required)
- Redirect — The mesh is ready. Download it from the pre-signed S3 URL. For GLB: the URL points to a binary GLB file; For OBJ: the URL points to a ZIP archive (.obj + .mtl + texture files)
data(object, required) — The mesh is ready. Download it from the pre-signed S3 URL. For GLB: the URL points to a binary GLB file; For OBJ: the URL points to a ZIP archive (.obj + .mtl + texture files)data.url(string, required) — Pre-signed S3 download URL for the generated mesh
event(string (redirect), required)
- RetryAfter — The mesh is still being generated. Wait the specified number of seconds, then call the endpoint again. Typical generation takes 10–60 seconds depending on mesh options.
- 401 — Missing, malformed, or invalid API key
- 404 — No avatar found
- 405 — Unsupported request. Possibly outdated or wrong format. Please check current API documentation
- 422 — Avatar reconstruction failed. Try initialising a new one with different photos
- 425 — Avatar is reconstructing. Please try again later
- 500 — Unexpected internal error. Please check documentation or try again later
- 502 — Cloud infrastructure encountered an unexpected error. Please try again later
GET /v1/avatar/{avatar_id}/get-info
Returns reconstruction metadata including estimated camera positions and projections for each input image.
Camera matrices are returned as 4×4 nested arrays in row-major order. Entries are null for images that failed processing.
- camera_positions: World-to-camera (view) matrices. To get camera-to-world for 3D reconstruction, transpose (row-major → column-major) then invert.
- camera_projections: Intrinsic projection matrices (NOT standard OpenGL format). Encodes focal length in pixels and image dimensions. See API description for decomposition formulas.
img_urls contains pre-signed download URLs for the preprocessed input images.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
avatar_id |
path | yes | string | Avatar ID. |
Responses
- 200 — Avatar reconstruction metadata
camera_positions(array of array | null, required)camera_projections(array of array | null, required)expressions_enabled(boolean, required) — Whether the avatar was created with facial expressions enabledfocal_length_type(string (manual, exif, estimated_common, estimated_per_image), required) — The focal length method that was actually used during reconstruction. Note: this is an OUTPUT field and may differ from the input focal_length_type: "manual": user-provided focal length values were used; "exif": all photos had valid FocalLengthIn35mmFilm EXIF data, which was used instead of estimation (possible when input was "estimate_common" or "estimate_per_image"); "estimated_common": a single shared focal length was estimated; "estimated_per_image": individual focal lengths were estimated per image (EXIF data was not found)img_urls(array | null, required) — URLs for downloading preprocessed images.
- 401 — Missing, malformed, or invalid API key
- 404 — Avatar not found
- 422 — Avatar reconstruction failed. Try initialising a new one with different photos
- 425 — Avatar is not ready yet
- 502 — Cloud infrastructure encountered an unexpected error. Please try again later
DELETE /v1/avatar/{avatar_id}
Permanently deletes the avatar together with all its photos and generated 3D assets
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
avatar_id |
path | yes | string | Avatar ID. |
Responses
- 200 — Avatar deleted
- 401 — Missing, malformed, or invalid API key
- 404 — Avatar not found
- 423 — Cannot delete avatar - processing in progress
- 502 — Cloud infrastructure unexpected error. Please try again later