Image
Layer separation (PSD)
Upload a character image and receive a layered PSD — back hair, front hair, face, eyes, mouth, clothing and more on separate layers, with hidden areas filled in. Ready for rigging and animation.
/v1/images/layersSeparation takes a few minutes, so it is asynchronous: the request returns a job ID right away (202 Accepted), and you poll GET /v1/images/layers/{id} until the job finishes. The request is multipart/form-data because it carries a file.
curl https://api.xkiro.com/v1/images/layers \
-H "Authorization: Bearer $XKIRO_API_KEY" \
-F image=@./character.png \
-F resolution=1280{
"id": "9b4427ea-56b0-416a-8529-2ac00f30ef8d",
"object": "image.layers",
"model": "xkiro/psd-layers",
"status": "queued",
"created_at": 1793334189,
"resolution": 1280,
"seed": 42,
"split_sides": false
}| Field | Type | Description |
|---|---|---|
imagerequired | file | Exactly one image: PNG, JPEG or WebP, up to 50 MB, at least 64 px on each side and at most 50 megapixels. Very large images are downscaled before processing — the output canvas is at most 1280 × 1280, so nothing is lost. The type is detected from the file's bytes, not its name. Damaged or truncated files are rejected with 400 before anything is charged. |
model | string | Defaults to xkiro/psd-layers, currently the only layer-separation model. |
resolution | integer | Working resolution of the output canvas: 768 to 1280 in steps of 64. Default 1280. Higher values keep more detail in small parts such as eyes and hands. |
seed | integer | 0–9999, default 42. The same image, resolution and seed give the same result; change the seed to get a different take on ambiguous regions. |
split_sides | boolean | Default false. When true, paired parts — hands, eyes, ears — are split into separate left and right layers. |
What input works best
One character, full body, facing the camera, on a transparent or plain background. A PNG with transparency gives the cleanest layer edges. Scenes with several characters or busy backgrounds produce poor separations.
Getting the result#
/v1/images/layers/{id}curl https://api.xkiro.com/v1/images/layers/9b4427ea-56b0-416a-8529-2ac00f30ef8d \
-H "Authorization: Bearer $XKIRO_API_KEY"{
"id": "9b4427ea-56b0-416a-8529-2ac00f30ef8d",
"object": "image.layers",
"model": "xkiro/psd-layers",
"status": "in_progress",
"created_at": 1793334189,
"resolution": 1280,
"seed": 42,
"split_sides": false,
"queue_position": 0,
"estimated_completion_at": 1793334280,
"completed_at": null,
"psd": null,
"layers": [],
"error": null
}{
"id": "9b4427ea-56b0-416a-8529-2ac00f30ef8d",
"object": "image.layers",
"model": "xkiro/psd-layers",
"status": "succeeded",
"created_at": 1793334189,
"resolution": 1280,
"seed": 42,
"split_sides": false,
"queue_position": null,
"estimated_completion_at": null,
"completed_at": 1793334357,
"expires_at": 1793938989,
"psd": {
"url": "https://cdn.xkiro.com/ai-image-layers/out/9b4427ea-…/layers.psd",
"bytes": 7882354
},
"layers": [
{ "name": "back hair", "url": "https://cdn.xkiro.com/ai-image-layers/out/9b4427ea-…/layer-01-back-hair.webp" },
{ "name": "bottomwear", "url": "https://cdn.xkiro.com/ai-image-layers/out/9b4427ea-…/layer-02-bottomwear.webp" },
{ "name": "ears", "url": "https://cdn.xkiro.com/ai-image-layers/out/9b4427ea-…/layer-03-ears.webp" }
],
"error": null
}| Field | Type | Description |
|---|---|---|
status | string | queued → in_progress → succeeded or failed. |
queue_position | integer | null | Position in the processing queue while the job runs (0 = being processed). null before the job reaches the queue and after it finishes. |
estimated_completion_at | integer | null | Unix timestamp (seconds) when the job is expected to finish: the later of the processing queue's own estimate and the run time we have measured on recent jobs at the same resolution (about 3 minutes at 1280). It is an estimate, not a deadline — use it for progress display. null before the job reaches the queue and after it finishes. |
expires_at | integer | null | Unix timestamp (seconds) until which the result files are kept — 7 days after the job was created. After that the files are deleted and psd / layers come back empty. null until the job succeeds. |
psd | object | null | The layered Photoshop file: url and size in bytes. Present only when the job succeeded. The canvas is square at the requested resolution; the character is centred and padded. |
layers | array | A full-canvas preview image (WebP) for every layer, with its name — handy for showing the result without parsing the PSD. The same fixed set of part categories is always returned (24 today, e.g. back hair, face, irides, topwear, footwear, wings); a layer is blank when the character has no such part. Empty array until the job succeeds. |
error | object | null | { code, message } when the job failed — see error codes. Always null for a succeeded job. |
Files are kept for 7 days
The psd.url and layers[].url links are served by xKiro and stay available until expires_at (7 days after the job was created); then the files are deleted for good. Download the PSD and keep your own copy. The links need no API key and are hard to guess, so treat them as private.
Polling correctly
async function separateLayers(file: Blob, fileName: string) {
const form = new FormData();
form.append("image", file, fileName);
form.append("resolution", "1280");
const headers = { Authorization: `Bearer ${process.env.XKIRO_API_KEY}` };
// No Content-Type header: fetch sets it, including the multipart boundary.
const job = await fetch("https://api.xkiro.com/v1/images/layers", {
method: "POST",
headers,
body: form,
}).then((r) => r.json());
if (job.error) throw new Error(job.error.message);
// A job normally takes about 3 minutes, longer when the queue is busy.
// Always bound the wait.
const deadline = Date.now() + 30 * 60_000;
while (Date.now() < deadline) {
await new Promise((r) => setTimeout(r, 10_000));
const res = await fetch(`https://api.xkiro.com/v1/images/layers/${job.id}`, { headers })
.then((r) => r.json());
if (res.status === "succeeded") return res; // download res.psd.url now
if (res.status === "failed") throw new Error(res.error.message);
}
throw new Error("Layer separation timed out");
}import time, requests
headers = {"Authorization": f"Bearer {API_KEY}"}
with open("character.png", "rb") as f:
job = requests.post(
"https://api.xkiro.com/v1/images/layers",
headers=headers,
files={"image": ("character.png", f, "image/png")},
data={"resolution": "1280"},
).json()
while True:
time.sleep(10)
res = requests.get(f"https://api.xkiro.com/v1/images/layers/{job['id']}", headers=headers).json()
if res["status"] == "succeeded":
with open("character.psd", "wb") as out:
out.write(requests.get(res["psd"]["url"]).content)
break
if res["status"] == "failed":
raise RuntimeError(res["error"]["message"])Listing your jobs#
/v1/images/layersYour recent jobs, newest first (limitup to 100, default 20). Pass the previous page's next_before as before to page back.
{
"object": "list",
"data": [ /* job objects, newest first */ ],
"next_before": "2026-10-30T04:23:09.104Z"
}Billing#
- $0.40 per successful job. Failed jobs are not charged.
- The amount is reserved from your wallet when the job is accepted and charged only when it succeeds; otherwise the reservation is released. Requests are rejected with
402when the wallet cannot cover one job. - Plan allowances do not apply — layer separation is always paid from the wallet.
Limits#
- Up to 35 jobs in progress per account, processed in parallel. A 36th request is rejected with
429until one finishes. - Uploads larger than 10 MB count toward a small per-account limit on simultaneous large uploads; past it you get
429withRetry-After. The upload slot is released as soon as the job is accepted, so sending large images back to back is fine. - Processing capacity is limited. When it is used up, new requests are rejected with
503before anything is reserved — retry later.
Error codes#
Errors at submission use the standard error format (400, 401, 402, 404, 413, 429, 503). A job that fails after it was accepted reports one of these in error.code:
| Field | Type | Description |
|---|---|---|
capacity_exhausted | retry later | No processing capacity was available. Not charged. |
upstream_unavailable | retry later | The processing service was temporarily unavailable. Not charged. |
processing_failed | retry | Processing failed for this image. Not charged; retrying, possibly with another seed, usually works. |
timeout | retry | The job did not finish in time. Not charged. |
interrupted | resubmit | The job was interrupted by a service restart. Not charged — submit it again. |
invalid_image | fix input | The image could not be processed. Use a PNG, JPEG or WebP of a single character. |
Licence and commercial use#
The layer-separation model is licensed under Open RAIL licences (CreativeML Open RAIL++-M and Open RAIL-M). Commercial use is permitted. The licensor claims no rights in the output you generate: the PSD and layer images are yours to use, including commercially, and you are accountable for them and their subsequent uses. Rights in the image you upload stay with whoever holds them — only upload images you have the right to use.
Acceptable use#
The licence's use-based restrictions are part of the xKiro Terms of Service and apply to every request to this endpoint. You agree not to use the model or its output:
- In any way that violates any applicable national, federal, state, local or international law or regulation;
- For the purpose of exploiting, harming or attempting to exploit or harm minors in any way;
- To generate or disseminate verifiably false information and/or content with the purpose of harming others;
- To generate or disseminate personal identifiable information that can be used to harm an individual;
- To defame, disparage or otherwise harass others;
- For fully automated decision making that adversely impacts an individual's legal rights or otherwise creates or modifies a binding, enforceable obligation;
- For any use intended to or which has the effect of discriminating against or harming individuals or groups based on online or offline social behavior or known or predicted personal or personality characteristics;
- To exploit any of the vulnerabilities of a specific group of persons based on their age, social, physical or mental characteristics, in order to materially distort the behavior of a person pertaining to that group in a manner that causes or is likely to cause that person or another person physical or psychological harm;
- For any use intended to or which has the effect of discriminating against individuals or groups based on legally protected characteristics or categories;
- To provide medical advice and medical results interpretation;
- To generate or disseminate information for the purpose to be used for administration of justice, law enforcement, immigration or asylum processes, such as predicting an individual will commit fraud/crime commitment (e.g. by text profiling, drawing causal relationships between assertions made in documents, indiscriminate and arbitrarily-targeted use).
See also Image generation and Image editing.
