API reference
Plug-and-play REST. Smart defaults: you pass character_id + prompt, we handle the rest.
Quickstart
Three steps from zero to a generated image:
- Create an API key on your Profile page.
- Create a persistent character via
POST /v1/characterswith a trait combo, and we'll generate the base avatar. - Generate as many follow-up images as you want via
POST /v1/images:generateusing the returnedcharacter_id.
End-to-end cURL (replace $XAVIRA_API_KEY):
curl -X POST https://api.xavira.ai/v1/characters \
-H "Authorization: Bearer $XAVIRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model_id": "realistic-sharp-v1",
"traits": {
"gender": "female",
"ethnicity": "east-asian",
"ageRange": "23-29",
"hairLength": "long",
"hairColor": "black",
"build": "slim",
"breastSize": "medium"
},
"name": "Sora"
}'
# → returns character_id
curl -X POST https://api.xavira.ai/v1/images:generate \
-H "Authorization: Bearer $XAVIRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"character_id": "<from above>",
"prompt": "sitting in a sunlit kitchen, casual sweater, candid"
}'
Authentication
Every request requires an API key in the Authorization header. Mint keys on your Profile page. Use a separate key per environment (production / staging / CI).
Authorization: Bearer xav_live_<your-key>
Keys are hashed at rest. The plaintext is shown once at creation time; lose it and you'll need to revoke + regenerate.
Models
Three production models, one API. Pick the model_id you want, and we handle the rest.
duration param. Async, webhook + poll.
Endpoints
All endpoints accept JSON, all return JSON. Base URL: https://api.xavira.ai.
POST /v1/characters
Create a persistent character by passing a closed-enum trait combo. We generate the base avatar and remember the identity for every subsequent generation against this character.
Body
One of realistic-sharp-v1, anime-pure-v1.
Closed-enum traits: see the trait enum table below. Closed enums prevent prompt-injection via trait input and give predictable output across customers using the same combo.
English only (see prompt tips). Free-text setting/outfit/pose description appended to the generated prompt. This is where you express everything the closed-enum traits don't cover: eye color, freckles, tattoos, lighting, mood, clothing details, accessories, makeup, pose, setting. See scene vocabulary below for what works. Run through moderation Tier 1 (rules) + Tier 2 (AI text classifier).
Generate a clothed, safe-for-work base avatar. The realistic avatar is a headshot and already tame, but anime trait combos (voluptuous / large bust) can drift revealing, so sfw: true prepends a model-aware clothing cue and appends a hard nudity-blocker negative to the avatar prompt. Works for both realistic-sharp-v1 and anime-pure-v1. This only affects the one-time avatar; per-generation SFW is the separate sfw flag on /v1/images:generate. For best results name a closed casual garment plus bottoms in scene (e.g. "wearing a crew-neck sweater and jeans"). Avoid "blazer", "blouse" or "business/office" wording, which the realistic model tends to render as an open jacket over a bare chest.
Customer-facing label shown in your dashboard.
Arbitrary JSON for your own bookkeeping (internal ID, tag, etc).
Pass false to create the character asynchronously: we answer 202 in under a second with a character_id and a generation_id, and render the avatar in the background. See async creation below.
Only with wait: false. We POST the finished (or failed) generation here so you don't have to poll.
Avatars are always max quality. Character creation always renders athd_portrait(896 × 1152) withhires_fix: "1.5x"on. The base avatar is a one-time investment that conditions every subsequent generation for this character, with no reason to expose a "downgrade" knob. Per-genresolutionandhires_fixare still customer-settable on/v1/images:generate.
No reference-image uploads. Characters are created exclusively from trait combos. Uploading photos for adult-content generation creates legal exposure (image-based sexual abuse / NCII) we deliberately don't accept on the platform. All identity comes from prompts and trait selections.
Trait enum table
female · maleDetermines pronouns and gendered descriptors in the prompt.white · black · hispanic · middle-eastern · indian · east-asian · south-east-asianMaps to natural-language descriptors (realistic) or skin-tone tags (anime).18-22 · 21-22 · 23-29 · 30-39 · 40-plusAdult-only ranges by design. All values map to clearly-adult descriptors; under-18 is hard-blocked by moderation. 21-22 exists for platforms whose own terms set a floor above 18: without it the only compliant option was 23-29, which loses the youngest adult look entirely.short · medium · long—black · brown · blonde · red · auburn · grey · white—petite · slim · athletic · curvy · voluptuousAdult body types only. Petite means "small adult", never "child-like".small · medium · large · very-large · hugeOnly applies when gender=female. Ignored for male. large is the middle of the scale, not the top.small · medium · large · very-large · hugeOptional, independent of build. Only applies when gender=female. large is the middle of the scale.Response (201)
{
"character_id": "151fccbd-4667-43fc-9cc0-fe2be6b7d1b8",
"avatar_url": "https://pub-202d3c54....r2.dev/.../avatar.png"
}
Store the character_id: it's the only field you need to pass back on subsequent generations. Identity is preserved automatically from there.
Async creation (wait: false)
By default this endpoint holds the connection open until the avatar exists. That is usually 8 to 16 seconds, and it is capped at 26 by our gateway. Two things break on that: a proxy or load balancer in front of your app with a shorter idle timeout will cut the request while we are still rendering (you see a 504 that never came from us), and a cold GPU worker takes longer than 26 seconds no matter what. Pass wait: false and neither can happen.
curl -X POST https://api.xavira.ai/v1/characters \
-H "Authorization: Bearer $XAVIRA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: your-own-character-row-id" \
-d '{"model_id":"realistic-sharp-v1","wait":false,"traits":{"gender":"female","ethnicity":"hispanic","ageRange":"23-29","hairLength":"long","hairColor":"blonde","build":"curvy"}}'
Response (202)
{
"character_id": "151fccbd-4667-43fc-9cc0-fe2be6b7d1b8",
"generation_id": "9e1a7870-89b0-47cd-bc9a-60bdf42ecbf2",
"status": "pending",
"avatar_url": null,
"cost_credits": 1,
"poll_url": "/v1/generations/9e1a7870-89b0-47cd-bc9a-60bdf42ecbf2"
}
The character_id is final from this moment: store it against your own record straight away. Then poll GET /v1/generations/{generation_id} (or wait for your callback_url) until status is completed, at which point output_url holds the avatar. Every second is plenty as a poll interval.
The character is not usable until the avatar lands./v1/images:generateand/v1/videos:generateanswer409 character_pendingwhile it is still rendering, because the avatar is the face every later generation is conditioned on.
A failed render costs nothing. The credit is taken when you call, and returned in full if the avatar never arrives, whether it failed outright or simply never reported back. You will see the generation instatus: "failed"with the reason inerror_message, and the character retired. Retrying with the sameIdempotency-Keyis allowed after a failure: a refunded attempt releases its key.
PATCH /v1/characters/{character_id}
Change a character's trait combo without creating a new one. The avatar and the face are untouched: this changes the description that goes into every prompt, not who is in the picture, so the character_id and everything pointing at it keeps working.
Body
A complete closed-enum trait combo, same shape POST accepts. See the trait enum table. Replaces the existing combo outright rather than merging into it.
Rename the character.
Pass at least one of the two, or the request is rejected with nothing_to_update.
The avatar is not re-rendered. On a character you created from a trait combo, the avatar was generated from the old traits, so changing them lets the two drift apart. That is allowed and sometimes what you want; if you need the avatar to match, create a new character instead.
curl -X PATCH https://api.xavira.ai/v1/characters/151fccbd-4667-43fc-9cc0-fe2be6b7d1b8 \
-H "Authorization: Bearer $XAVIRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"traits":{"gender":"female","ethnicity":"white","ageRange":"23-29","hairLength":"long","hairColor":"blonde","build":"slim","breastSize":"large"}}'
Response (200)
{
"character_id": "151fccbd-4667-43fc-9cc0-fe2be6b7d1b8",
"name": "Ava",
"model_id": "realistic-sharp-v1",
"avatar_url": "https://pub-202d3c54....r2.dev/.../avatar.png",
"traits": { "gender": "female", "...": "..." },
"created_at": "2026-08-20T21:48:34.281Z"
}
A character_id that is not on your account returns 404 character_not_found.
POST /v1/images:generate
Generate a new image against an existing character. Sync-first, async fallback: most gens complete inline (HTTP 201, full output_url in the response). When a gen takes longer than our sync budget (~23s) we return HTTP 202 with a poll URL, and you call GET /v1/generations/:id until the status flips to completed.
Body
Returned from POST /v1/characters. The model + face are inherited from the character.
English only (see prompt tips) — other languages are accepted but unsupported, and degrade the pose you get. Free-text. Describe the scene / pose / outfit / lighting; we already encode the character identity.
Pick from a curated set of size + orientation presets. Default hd_portrait (896×1152) for maximum detail; pass sd_portrait for a faster, lighter render. Every preset costs the same single credit. See resolution presets below for all six options.
Sharpening pass after base render. "1.5x" (default) = max detail. "1.25x" = faster, smaller output, slightly less micro-detail. Both produce integer pixel counts on every resolution preset. See the presets table.
Name the pose template yourself instead of letting us infer it from your prompt. Pass any key from known pose aliases (e.g. "doggy", "pov-oral"), or "none" to apply no pose template at all while keeping the quality prefix and your character's traits.
By default we infer the pose from your text, and inference is the main reason a result can differ from what you pictured: the matcher has to pick the closest key it knows, so a request that isn't really any of them still lands on one. pose removes the guesswork. Use "none" when you want to describe the composition entirely yourself — camera angle, body position, framing — and keep identity consistency, which raw_prompt cannot do because it drops the traits string too. Cannot be combined with raw_prompt (400 conflicting_params); an unknown key returns 400 invalid_pose with the full list.
By default we enhance your prompt server-side: we prepend a quality prefix + the character's traits string, and we match common pose aliases (e.g. "blowjob pov" expands to the full POV-oral prompt with weighted camera angles and anatomy anchors). Set raw_prompt: true to ship your prompt verbatim, useful if you already include your own quality cues or pose detail and want full control over what reaches the model.
Removed from this endpoint: it did not reliably produce clothed images. Still accepted so existing integrations keep working, but it has no effect and the response includes a warnings entry. See Generating SFW (clothed) images for the approach that replaces it. (sfw on /v1/characters is unaffected.)
Keep the matched pose but remove the nudity. Where sfw drops the pose entirely, clothed_pose: true applies the pose composition (from behind, straddling, on all fours, looking back, …) with the character dressed, then adds a clothing cue + nudity-blocker negative. Use it for a persona in an NSFW pose while staying dressed. Explicit acts, including oral, keep the act and the composition; positional and suggestive poses keep theirs.
Ask for a post-generation read on whether the character came out dressed. The response then carries clothed (boolean) and clothed_confidence (0-100, where low means worth a look). Realistic model only, and off by default, since most traffic here is intentionally explicit and should not pay for the extra step. On a 120-image hand-labelled set it flagged every result with a bare breast visible, and sent back about 4% of clean images that did not need it, so a low score is worth acting on and a high one is worth trusting. Nothing is retried for you. If the score is low and you want another try, call this endpoint again; each call is billed. Retrying once on a low score took a 1-in-5 exposure rate to roughly 1 in 25 in our tests. Adds roughly a second to the call.
Appended to our house negatives (we already block deformed hands, watermarks, etc). Use only for additional aesthetic exclusions.
For reproducibility. Same seed + same prompt + same character + same pose = same output, within model determinism.
Pin pose (or raw_prompt) if reproducibility matters to you. Without it the pose is inferred per call, and inference is not part of the seed: two calls with identical inputs can resolve to different templates and therefore produce genuinely different images. With the pose pinned, the only remaining variation is the model's own.
Framing
Optional framing decides how much of the body is in shot. One value today: "full_body". Leave it out and nothing changes.
It exists because framing words stop working once a prompt gets long. On one scene over ten seeds, a full-length instruction in front of a short prompt gave head to toe 36 times out of 36; the same instruction in front of a 150-word paragraph gave 2 out of 10. framing spreads the instruction through the whole prompt instead of leaving it at the front, which takes that same paragraph to 7 out of 10. A short prompt is unaffected, it was already working.
Two things it does not do. It will not fix a prompt that argues with itself (asking for a close-up selfie and framing: "full_body"), and it is not a guarantee, so if head to toe is essential, keep the prompt short as well. The value is echoed back in the response, and an unknown one is a 400 invalid_framing rather than silence.
There is deliberately no "portrait" or "half". We built and measured both: each came back as a knee-length shot, near-indistinguishable from the other and from passing nothing. Repeating an instruction can push a frame wider, which is what the model does not do on its own, but it cannot pull one tighter, because the model already crops close. Rather than ship two values that quietly do nothing, we ship the one that works. For a tighter shot, say so in the prompt and keep the prompt short.
{ "character_id": "chr_...", "prompt": "walking down a city street at sunset, fitted black dress", "framing": "full_body" }
Resolution presets
Realistic gens run a hires-fix sharpening pass on top of the base render (default scale 1.5x; pass hires_fix: "1.25x" for the lighter / faster variant). Anime renders in a single pass at the base resolution, so hires_fix does not apply and the FINAL @ columns below are realistic-only.
All presets cost 1 credit regardless of hires-fix level. Custom width / height are not accepted: we curate the set so every output lands on quality-tested dimensions (integer pixel counts at every scale factor).
Response (201, completed inline)
{
"generation_id": "2e301acc-7a48-4f2f-8bb1-63c4320db7ae",
"character_id": "151fccbd-4667-43fc-9cc0-fe2be6b7d1b8",
"output_url": "https://pub-...r2.dev/.../gen.png",
"status": "completed",
"gen_time_ms": 1923,
"wall_time_ms": 4997,
"cost_credits": 1
}
Response (202, still running, poll for the result)
{
"generation_id": "b83bc79c-576b-442e-aed8-af8b7fcd43e4",
"character_id": "151fccbd-4667-43fc-9cc0-fe2be6b7d1b8",
"model_id": "realistic-sharp-v1",
"kind": "image",
"status": "pending",
"cost_credits": 1,
"poll_url": "/v1/generations/b83bc79c-576b-442e-aed8-af8b7fcd43e4"
}
When you get a 202, the gen is still running on the GPU. Call GET /v1/generations/:id with the returned id every 2-5 seconds until status is completed (output_url present), failed (refunded), or cancelled / timed_out. Credits are debited at submit and refunded automatically on terminal failure.
POST /v1/videos:generate
Async I2V (image-to-video). By default uses the character's avatar as the first frame; pass generation_id to animate a specific prior image instead. Returns 202 immediately with a generation_id; poll GET /v1/generations/:id or receive a webhook at callback_url.
Body
Must be an image-model character. Anime characters work too. Required even when you pass generation_id; used for ownership + tracking.
Animate a specific prior generation instead of the character avatar. Must be one of your own completed generations.
An image: its output_url becomes the first frame. Use this to vary the starting pose, outfit or scene without creating a new character.
A video: its stored final frame becomes the first frame, so the next clip starts on the pose, wardrobe and lighting the previous one ended on. This is how you chain clips. A clip rendered before this shipped has no stored frame and returns 400 no_last_frame; render a fresh one and chain from that. Every completed video also returns last_frame_url if you want the still itself.
English only (see prompt tips). Describe the motion you want, not the scene. E.g. "slow head turn, hair gently flowing". If omitted, the API uses a safe anti-drift default that keeps the character + scene anchored ("same scene, subtle motion, gentle breathing"). Power users can still pass their own motion prompt for full control.
Same idea as for images.
Reproducible motion.
Clip length. "5s" (default, 5 credits) or "10s" (10 credits). Delivered at 32 fps in both cases (rendered at 16 fps, frame-interpolated 2×), so the clip is smooth without being longer. 10s gens take roughly 2× the wall-clock of 5s on the same hardware.
We POST a JSON payload here when the gen completes. See webhook payload below. 5 retries with exponential backoff (1/2/4/8/16 min) before abandoning.
Two-step example: animate a generated image
# 1. Generate an image you like
curl -X POST https://api.xavira.ai/v1/images:generate \
-H "Authorization: Bearer $XAVIRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"character_id": "<char-id>",
"prompt": "leaning against a rooftop railing at blue hour, silk slip dress"
}'
# → { "generation_id": "2e301acc-...", "output_url": "https://...", ... }
# 2. Animate that exact image
curl -X POST https://api.xavira.ai/v1/videos:generate \
-H "Authorization: Bearer $XAVIRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"character_id": "<char-id>",
"generation_id": "2e301acc-...",
"prompt": "slow head turn, hair gently flowing"
}'
# → { "generation_id": "b83bc79c-...", "status": "pending", ... }
Response (202)
{
"generation_id": "b83bc79c-576b-442e-aed8-af8b7fcd43e4",
"kind": "video",
"status": "pending",
"cost_credits": 5,
"poll_url": "/v1/generations/b83bc79c-576b-442e-aed8-af8b7fcd43e4"
}
Webhook payload (POST to your callback_url)
POST <callback_url>
Headers:
X-Xavira-Generation-Id: <uuid>
X-Xavira-Delivery-Id: <uuid>
X-Xavira-Attempt: 1
X-Xavira-Signature: t=1779350400,v1=a3f4...e9b # HMAC-SHA256, see below
Body:
{
"type": "video.completed",
"generation_id": "b83bc79c-...",
"character_id": "151fccbd-...",
"status": "completed", // or "failed"
"output_url": "https://pub-...r2.dev/.../video.mp4",
"gen_time_ms": 83582,
"wall_time_ms": 311,
"cost_credits": 5,
"error_message": null
}
Your endpoint should respond with a 2xx within 5 seconds. Any other status (or timeout) counts as failure; we'll retry with exponential backoff. After 5 failures the delivery is marked abandoned; you can still pull the generation via the poll endpoint. Always verify the signature before trusting the payload; see Webhook verification.
GET /v1/generations/:id
Poll a generation. Lazy completion: if status is still pending, we check the upstream worker and mutate the DB on terminal state (idempotent, safe to poll concurrently). On FAILED the credits are automatically refunded.
{
"generation_id": "b83bc79c-576b-442e-aed8-af8b7fcd43e4",
"kind": "video",
"status": "completed",
"output_url": "https://pub-...r2.dev/.../video.mp4",
"gen_time_ms": 83582,
"wall_time_ms": 311,
"cost_credits": 5
}
For sync image gens this is just a DB read. Suggested poll cadence for videos: every 5-10 seconds, max wall-clock budget ~3 minutes (cold start + ~80s gen).
Prompt enhancement (default on)
Every POST /v1/images:generate call runs your prompt through a server-side enhancement layer before it reaches the model. This is on by default; set raw_prompt: true to opt out.
What enhancement does
- Pose alias matching. Short labels like
"blowjob pov","doggy","reverse cowgirl"are matched against a curated pose library and swapped for the fully-expanded prompt, with camera angles, weighted tokens, and anatomy anchors. Without this, a short prompt like"blowjob pov"gives the model too little signal and it defaults to a portrait. - Identity anchor. The character's stored traits (
age+ethnicity+hairLength+hairColor+breastSize) are formatted into a natural-language phrase and prepended, e.g."26 year old Hispanic Latina woman, tan brown skin, long brown hair, medium breasts, ". Improves consistency across generations. - Quality prefix. A fixed string is prepended:
"RAW photo, DSLR, 8k uhd, film grain, natural skin texture, visible pores, ...", counteracting the model's AI-stockphoto bias and produces noticeably more "real photo" output. - Negative additions. A fixed
FACEID_NEGATIVE_PROMPTcovering common artifacts (mirror reflections, ghost breasts, watermarks, deformed anatomy, etc.) is appended to yournegative_prompt_appendon top of the model's own house negatives.
Transparency: the enhanced_prompt response field
The 201 response includes:
{
"generation_id": "...",
"output_url": "...",
"enhanced_prompt": "RAW photo, DSLR, 8k uhd, ... 26 year old Hispanic Latina woman, ... POV from above, deepthroat, ...",
"matched_pose": "pov-oral",
...
}
matched_pose is null when your input didn't hit a known alias; in that case enhanced_prompt is just QUALITY_PREFIX + traits + your_prompt_verbatim. Suppressed entirely when raw_prompt: true since nothing was modified.
Known pose aliases
Common phrasings get matched (case-insensitive, substring search). Sample aliases below; see matched_pose in the response to confirm a match.
A neutral prompt stays neutral. Until 2026-08-18 the matcher had to pick the nearest key from a closed list with no neutral posture in it, so an ordinary request had nowhere to go: "kneeling on a wooden floor, hands resting on her thighs, looking at the camera" resolved to kneeling-bj in 5 of 6 measured runs, and "sitting cross-legged on a rug, reading a book" to lying-down in 6 of 6. Three things changed. The matcher can now answer "none of these", and a sexual pose it returns for a prompt carrying no sexual cue is discarded. There are neutral kneeling / sitting / crouching / standing-portrait templates that state posture and framing and nothing else. And ten templates written for a male subject are refused on a female character, which is where unrequested anatomy in those renders came from. Naming an act explicitly is unaffected: "spread ass" and "giving head" are clear requests and still resolve.
Literal aliases now take precedence over the language model that used to run first, so resolution is deterministic. That is what makes the seed guarantee above true rather than approximately true.
Building custom poses (no alias match)
If your prompt doesn't hit any alias, we still apply the quality prefix + traits + negatives, but your prompt itself ships verbatim as the scene description. To get good results for a custom pose:
- Be specific: camera angle + body position + key anatomy details + lighting. E.g.
"yoga child's pose, viewed from behind, soft natural morning light, hands stretched forward, ass slightly raised"works better than"yoga pose". - Use weighted tokens for the elements you want emphasized:
"(arched back:1.4)","(eyes locked on camera:1.3)". - Check
enhanced_promptin the response to see exactly what reached the model. Iterate from there. - For tightest control, pass
raw_prompt: true: you take full ownership of the prompt; only the model's house-negatives stack on top.
When to use raw_prompt: true
- You already include your own quality cues and weighted tokens.
- You're building your own prompt-construction layer and want our backend to stay out of it.
- You're A/B-ing alternatives: disable enhancement, compare against the default.
Generating SFW (clothed) images
realistic-sharp-v1 is tuned for adult output, so a clothed result is something you steer, not something you get by default. It is very achievable, but the way you phrase the request matters more than most people expect. Follow the three rules below and clothed output becomes the norm rather than the exception.
The recipe
Since 2026-08-16 we apply the first two rules for you when a request names a garment and little else: a bare "wearing a blue t-shirt" is expanded with a neckline, a bottom half and a setting before it reaches the model, and a crop you asked for is no longer overridden. Writing it out yourself still gives you the most control, and the rules below are exactly what we do on your behalf.
Part of the third rule is now automatic too. Naming any garment adds the lifting family to the negative on our side (lifting shirt, shirt pulled up, flashing, undressing, open shirt, unbuttoned and the rest), because the dominant failure mode was never nudity as such: the garment rendered correctly and the model then pulled it up, which every nudity-shaped blocker misses since nobody in that picture is undressed. When the named garment actually covers the torso we add the state tokens (bare breasts, topless) on top. We deliberately do not add those for a bottoms-only outfit, since "wearing a black thong" implies an uncovered top and blocking it would dress someone you left undressed on purpose. Sending the full list yourself is still worth doing: it is broader than what we add, covering swimwear, lingerie and crop tops that we cannot assume you want excluded.
- Name one ordinary, everyday garment plus bottoms. A crew-neck knit sweater, a t-shirt, a turtleneck, a plain dress. Be concrete:
"a cream crew-neck knit sweater and blue jeans"beats"clothed"or"modest outfit"by a wide margin. - Describe the setting and the light. A garment floating in a void gives the model very little to hold on to.
"standing in a sunlit living room, soft window light"anchors the whole picture. - Paste the blocker list into
negative_prompt_append. It is reproduced in full below. Copy it verbatim.
Some requests cannot be SFW under this recipe, and it is worth knowing which. The blocker list includes
swimwearandbikini, so asking for a bikini while sending the list means the same call is requesting and blocking the same thing. A single open garment ("wearing only a shirt") is the other case: an open front is the whole shape of the request. If your product needs either, generate them without the blocker list and review the result, rather than trying to phrase your way out of the conflict.
Full example
curl -X POST https://api.xavira.ai/v1/images:generate \
-H "Authorization: Bearer xav_live_<your-key>" \
-H "Content-Type: application/json" \
-d '{
"character_id": "<id>",
"prompt": "wearing a cream crew-neck knit sweater and blue jeans, standing in a sunlit living room, soft window light, full-body shot",
"resolution": "hd_portrait",
"negative_prompt_append": "nude, naked, topless, exposed breasts, exposed nipples, exposed chest, exposed cleavage, sheer fabric, see-through, deep plunging neckline, low-cut, open shirt, unbuttoned, lifting shirt, shirt pulled up, shirt lifted above breasts, shirt rolled up, shirt bunched above chest, pulling top up, holding shirt up, flashing breasts, flashing, exposing breasts, undressing, bralette, lingerie, underwear visible, swimwear, bikini, side boob, underboob, bare midriff, crop top"
}'
Any framing works
A common assumption is that you have to hide the torso to stay clothed. You don't. Close-up, waist-up and full-body all behave the same once the garment is phrased well, so frame the shot however your product needs it.
What to avoid, and why
Two phrasings look helpful and are actively counterproductive. Both are worth internalising, because they explain nearly every surprising result.
- Do not name a fastening. Write
"a grey sweatshirt", not"a zipped-up hoodie"and not"a shirt buttoned to the collar". Naming a zip or buttons puts a thing that opens into the picture, and the model will often open it. We now strip these words out of a garment clause before it reaches the model, so an older prompt of yours will behave better than it did. Writing them out is still the worse habit though, because the removal only covers wording we already know about. This is also why blazers, blouses, suits and coats are the hardest case: an open front is part of what those words mean. If your scene needs a jacket, put it over a named closed layer and expect to check the result. Worth knowing before you spend credits on it: this failure is not occasional, it is a property of the wording. We ran one tweed jacket prompt on three different seeds and all three came back the same way, so generating again will not rescue it. Change the wording, not the seed. - Do not phrase anything as a negation in
prompt."with no zip","not revealing"and"covered chest"all tend to summon exactly what they were meant to exclude, because the positive prompt has no concept of "not". Everything you want excluded belongs innegative_prompt_append, never inprompt.
Checking a result, and when retrying helps
Pass clothed_confidence: true and the response carries a clothed boolean and a 0-100 score. In practice the score comes back either at or above 90, or at or below 10, with nothing in between, so treat it as a yes or no rather than a dial.
Nothing is retried for you. If you want a second attempt, that is a loop in your own code, and it is short:
let out;
for (let i = 0; i < 3; i++) {
out = await generate({ ...req, seed: baseSeed + i, clothed_confidence: true });
if (out.clothed) break;
}
Retrying only helps a prompt that usually works and occasionally slips. A prompt that fails on every seed, which is what jackets and blazers do, will fail every attempt and only spend the credits. When two attempts fail the same way, treat that as a signal to change the wording rather than to raise the retry count.
NSFW poses with clothes on
If you want the composition of an adult pose but the character dressed, that is a different feature: pass clothed_pose: true. It keeps the matched pose and strips the nudity out of it. See the clothed_pose parameter.
If you need a hard guarantee
The recipe above is steering, not a switch, and this model is adult-tuned by design. If a single unexpected image would be a serious problem in your product, either validate output on your side before showing it, or use anime-pure-v1, which is reliably clothed. Character avatars are also reliably clothed: they are headshots, and /v1/characters still accepts sfw: true for that one-time avatar.
sfw parameter on /v1/images:generate has been removed. It did not reliably produce clothed images, and because it silently did nothing when passed as a string ("true" instead of true), integrations could ship against it without ever seeing an error. The parameter is still accepted so nothing breaks, but it is ignored and the response carries a warnings entry pointing here. Use the recipe on this page instead.
Scene vocabulary
The closed-enum traits cover identity (gender · ethnicity · age · hair · build). Everything else (eye color, freckles, makeup, tattoos, accessories, lighting, mood, outfit details, pose, location) lives in the free-text scene field (when you create the character) and in prompt (when you generate). That keeps the API surface small while letting you express anything natural language can.
This isn't a closed list. The model understands English. Here's a starter kit of phrases that consistently work; mix and match per gen.
Eye color & facial details
piercing blue eyes·warm brown eyes·green eyes·hazel eyes·grey eyes·amber eyeslight freckles across the nose·dense freckles·cheekbone frecklessmall mole above lip·beauty mark on cheek·cleft chin·dimplessoft jawline·strong jawline·high cheekbones·full lips·natural lipsglasses·cat-eye glasses·round wire glasses·aviator sunglasses
Makeup
natural no-makeup look·subtle makeup·nude lipstickwinged eyeliner·smokey eye·red lipstick·matte burgundy lipstickglossy lips·blush, freshly applied·bronzed cheeks
Tattoos & piercings
small floral tattoo on forearm·delicate line tattoos on arms·full sleeve tattooshoulder tattoo·collarbone tattoo·back tattoo, partially visiblenose stud·septum piercing·helix piercing·multiple ear piercings
Outfit & styling
- Casual:
oversized hoodie·cropped sweatshirt·vintage band tee·jeans and white sneakers - Workwear:
tailored blazer·linen button-down·pencil skirt·satin blouse - Eveningwear:
little black dress·silk slip dress·satin gown·backless top - Lingerie:
lace bralette·silk robe·black bodysuit·matching lingerie set - Hair styling:
wet hair·hair tied in a low bun·messy bun·side part·curtain bangs·blown-out hair
Lighting
- Natural:
golden hour·blue hour·soft morning light·overcast diffuse light·harsh midday sun·backlit by window - Studio:
three-point lighting·softbox key light·rim light·butterfly lighting - Cinematic:
moody low-key lighting·neon-lit night scene·candlelight·fire-lit warm tones·chiaroscuro
Mood / atmosphere
candid·posed editorial·spontaneous laughter·contemplative·flirty glance·seductive over-the-shoulder lookwarm and inviting·melancholic·energetic·intimate·powerful·vulnerable
Composition & framing
close-up portrait·medium shot·full body shot·three-quarter view·profileshallow depth of field·bokeh background·sharp focus throughoutshot on 35mm·shot on Hasselblad·iPhone selfie(each gives a distinct look)
Location / setting
- Indoor:
sunlit kitchen·industrial loft·cozy bedroom·luxury hotel suite·art gallery·vintage diner booth - Outdoor:
foggy coastline·autumn forest path·rooftop at dusk·desert at golden hour·cobblestone street in Paris - Time of day matters: always specify (
morning·afternoon·dusk·night): the model commits to a coherent lighting setup.
For anime-pure-v1: same idea, tag-style
Same vocabulary, but as comma-separated tags rather than prose. Examples:
blue eyes·heterochromia·freckles·mole under eye·fangtwintails·ponytail·messy hair·hair ornament·hair ribbonschool uniform alternative·maid outfit·kimono·casual·swimsuitlooking at viewer·looking away·upper body·cowboy shot·from belowsunset·night sky·cherry blossoms·indoor·outdoors
For video-v1 (video): describe motion
slow head turn·gentle smile spreading·hair flowing in breeze·eyes blinking·looking up to camerasips coffee·brushes hair behind ear·laughs softly·turns to look over shoulder
Heuristic. Two to four short comma-separated clauses beats one long sentence. The model treats prompts like a sequence of weighted features; long sentences dilute each feature's weight.
Prompt tips
Write prompts in English
Prompt and generate in English. That means prompt on images and videos, and scene on characters. English is the only language we support, and the only one that gives reliable results.
Other languages are not rejected, but the results are unreliable: you tend to get a different pose than you described, and in a non-Latin script (Persian, Russian, Japanese, Chinese, Arabic, Thai) often a plain portrait with nothing of your scene in it. If your own users write in other languages, translate to English in your stack before calling us. Non-Latin prompts return a warnings entry with code non_latin_prompt so you can catch it in your integration.
- ✅
on all fours on a bed, ass up, seen from behind, looking back over her shoulder at the camera, bedroom - ❌
یک دختر برهنه که چهار دست و پا است
For realistic-sharp-v1
Natural-language description works best. Lead with the scene, follow with lighting and style. Skip identity words (gender, age, hair); the character already encodes those.
- ✅
sitting at a wooden cafe table at golden hour, casual cardigan, candid photographic portrait - ✅
walking on a foggy boardwalk, leather jacket, cinematic lighting, shallow depth of field - ❌
a hispanic woman in her twenties with brown hair sitting in a cafe: identity duplicated; the character already encodes this. Style suffers.
Framing, and why prompt length decides it
Framing words work, but only while the prompt stays short. Put them at the very front and keep the rest to a line or two.
Measured over 46 generations on one scene: with full length shot, entire body from head to toe, feet and shoes visible, wide shot in front of a short prompt, 36 of 36 came back head to toe with the shoes in frame, across two characters and ten seeds. Put a 150-word identity paragraph behind those same words, on the same seeds, and it drops to 2 of 10. The instruction is present either way; in a long prompt everything else outweighs it.
- ✅
full length shot, entire body from head to toe, feet visible, wide shot, walking down a city street at sunset, fitted black dress, golden hour - ❌ the same thing as a paragraph, with the character's face, hair, age and build described again: the framing is still in there, it just loses.
The model draws words, it does not follow instructions
Anything phrased as a direction is read as something to render. vary the setting, randomly choose a pose and avoid repetitive framing do not vary or avoid anything; listing a bedroom, a bathroom and a pool in one prompt asks for all three at once.
- Pick the location and the pose in your code, send one of each, and change
seedper request. That is what gives you variety across a set. - Everything you want to avoid goes in
negative_prompt_append, never inprompt. Words likeplastic,CGIandAI-generatedin the prompt pull the image toward exactly those.
Outfits with explicit poses
Pose aliases (blowjob, cowgirl, etc.) expand to a curated template. Those templates default to nude, but naming an outfit switches the pose to its clothed form, so the act and the outfit no longer contradict each other. This works for oral poses too.
- Name the outfit as an outfit (
"in orange lingerie","wearing a schoolgirl uniform") and it is applied to the pose. In hetero poses the partner stays undressed so the act remains visible. - Displaced clothing gives you the most control over how much stays covered:
"orange lingerie pulled aside","bra pushed down","panties around one ankle". - Say nothing about clothing and the pose runs nude, as before.
clothed_pose: truekeeps the composition and the act while keeping the character dressed, including on oral poses.- Check
matched_posein the response to confirm the pose you intended was detected.
For anime-pure-v1
Tag style: comma-separated, no full sentences. Quality tags + subject + scene + style.
- ✅
masterpiece, best quality, sitting on rooftop, sunset, school uniform alternative, looking at viewer - ❌
she is sitting on a rooftop watching the sunset: full sentences underperform tag-style prompts here.
For video-v1 (video)
Describe the motion, not the scene; the avatar already sets the scene. Short clauses, mostly verbs.
- ✅
slow head turn, gentle smile, hair gently flowing in breeze - ✅
she sips coffee, looks up at camera, slight grin - ❌
a beautiful woman in a coffee shop in the morning: that's a scene description, you'll get minimal motion.
Idempotency
Send Idempotency-Key: <your-key> on any POST. Up to 100 chars, scoped per customer. A repeat with the same key returns the prior response: no duplicate charge, no duplicate gen.
Tip: use a UUID from your end so retries after a network drop are safe.
Webhook verification
Every webhook is signed with HMAC-SHA256 using your account's webhook secret (find it on the Profile page). The signature is sent in X-Xavira-Signature:
X-Xavira-Signature: t=1779350400,v1=a3f4d5e6...e9b
t is a Unix timestamp (epoch seconds). v1 is the HMAC-SHA256 hex of "<t>.<raw-body-bytes>".
Verification: Node.js
import crypto from 'node:crypto';
function verifyXaviraSignature(rawBody, signatureHeader, secret) {
const parts = Object.fromEntries(
signatureHeader.split(',').map(p => p.split('=')),
);
const t = parts.t, v1 = parts.v1;
if (!t || !v1) throw new Error('malformed signature header');
// Reject anything older than 5 minutes to prevent replay.
const age = Math.floor(Date.now() / 1000) - Number(t);
if (Math.abs(age) > 300) throw new Error('signature timestamp too old');
const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex');
// Constant-time compare.
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1))) {
throw new Error('signature mismatch');
}
}
// Express example — note: use raw body, NOT JSON.parse output.
app.post('/xavira-webhook', express.raw({ type: 'application/json' }), (req, res) => {
try {
verifyXaviraSignature(req.body.toString('utf8'), req.get('x-xavira-signature'), process.env.XAVIRA_WEBHOOK_SECRET);
} catch (e) {
return res.status(401).send(e.message);
}
const payload = JSON.parse(req.body.toString('utf8'));
// ... handle payload ...
res.status(200).send('ok');
});
Verification: Python
import hmac, hashlib, time
def verify_xavira_signature(raw_body: bytes, signature_header: str, secret: str) -> None:
parts = dict(p.split('=', 1) for p in signature_header.split(','))
t, v1 = parts.get('t'), parts.get('v1')
if not t or not v1:
raise ValueError('malformed signature header')
if abs(int(time.time()) - int(t)) > 300:
raise ValueError('signature timestamp too old')
expected = hmac.new(
secret.encode('utf-8'),
f"{t}.{raw_body.decode('utf-8')}".encode('utf-8'),
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expected, v1):
raise ValueError('signature mismatch')
Common pitfalls
- Sign the RAW request body, not
JSON.parseoutput. Re-stringifying produces different key order → signature mismatch. - Use constant-time comparison (
timingSafeEqual/hmac.compare_digest). Equality with==leaks the secret via timing side-channel. - Reject old timestamps (5-min window). Without that, an attacker who once captured a webhook can replay it forever.
- Rotate on suspected leak via the Profile page: old signatures stop verifying immediately.
Moderation
Three-tier moderation runs on every gen. Adult content is allowed by design: the API is built for it. We block exclusively for:
- Minors: any age cue under 18, child-coded features, school settings, loli/shota.
- Non-consent: drugged/unconscious, forced acts, "against will" framing.
- Bestiality, incest involving identifiable family relations, extreme sexual violence/gore.
Three tiers
- Tier 1, rule-based: ~150 lexical patterns. Runs in <1ms, fail-fast on obvious matches.
- Tier 2, text classifier: a fast third-party text classifier. Catches paraphrasing the rules miss.
- Tier 3, output vision classifier: a third-party vision classifier checks the generated image (realistic models only, anime classifiers hallucinate on cartoons). If flagged: image is deleted from R2, no credit charged.
Block response (422)
{
"error": {
"code": "moderation_blocked",
"message": "Blocked: prompt contains restricted content (category=minors)",
"details": { "tier": "rule", "category": "minors" }
}
}
When tier=ai or tier=output the category is free-form. Surface the message to your end-user verbatim.
Errors
Every error follows the same shape: { "error": { "code": "...", "message": "...", "details"?: {...} } }. Match on error.code, not on the message.
Every response carries a request id, successes included: the X-Request-Id header, and a request_id field in the body (on errors it is repeated inside error, so pasting the error object is enough). Log it. If you send it to us we can pull up that exact call, which is the difference between us reading your request and us guessing from a timestamp.
{
"error": {
"code": "moderation_blocked",
"message": "school setting",
"details": { "tier": "ai", "category": "ai_flagged" },
"request_id": "01M113S6FJ2BDRX5N0PBFG1V5E"
},
"request_id": "01M113S6FJ2BDRX5N0PBFG1V5E"
}
invalid_character_id, invalid_prompt, invalid_traits.character_id, OR it belongs to another customer. We don't leak which.wait: false and its avatar is still rendering. Poll its generation, then retry.Retry-After header (always < 60 sec). See Rate limits.Rate limits
Per-API-key, fixed 1-minute windows. Each key has independent counters; using a separate key per environment is the recommended pattern. The caps below are the Starter / Builder baseline; Scale doubles them (2×) and Volume quadruples them (4×). Enterprise caps are negotiable.
Response headers
Every gen response carries rate-limit headers so you can back off gracefully:
X-RateLimit-Limit: 60 # cap for this endpoint X-RateLimit-Remaining: 47 # calls left in the current window X-RateLimit-Reset: 1779350400 # epoch seconds when window resets
When you hit the limit
HTTP 429 rate_limited. Honour the Retry-After header (always < 60 seconds, that's the window size).
HTTP/1.1 429 Too Many Requests
Retry-After: 23
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1779350423
{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded — 60/min for /v1/images:generate. Retry in 23s."
}
}
Need more? Email hello@xavira.ai. We'd rather lift your cap than have you build retry queues. Enterprise plan offers per-key custom caps.
Spec & SDKs
Machine-readable spec + a Postman collection so you can poke at the API without writing code.
OpenAPI 3.1 spec
Full schema for every endpoint, request body, response shape, and error code. Import into Postman, Insomnia, openapi-generator, or any code-gen tool of your choice.
Download openapi.yaml View raw
Postman collection
Pre-wired collection with bearer auth, variable extraction (response character_id auto-saves to a collection variable, then the image-gen request references it), and idempotency-key auto-generation via {{$guid}}.
Set the apiKey collection variable before running. The collection runs Status → Create character → Generate image in that order; later requests pick up {{characterId}} automatically.
JavaScript / TypeScript SDK
Official Node.js SDK with typed request + response shapes, HMAC webhook verification, rate-limit-aware errors, and a pollUntilComplete helper for async video gens.
npm install @xavira/sdk
import { Xavira } from "@xavira/sdk";
const xavira = new Xavira({ apiKey: process.env.XAVIRA_API_KEY! });
const character = await xavira.characters.create({
model_id: "realistic-sharp-v1",
traits: { gender: "female", ethnicity: "east-asian", ageRange: "23-29",
hairLength: "long", hairColor: "black", build: "slim",
breastSize: "medium" },
});
const image = await xavira.images.generate({
character_id: character.character_id,
prompt: "sitting in a sunlit kitchen, casual sweater, candid portrait",
});
console.log(image.output_url);
Source + full README on GitHub.
Python SDK
On the roadmap. In the meantime, openapi-generator + openapi.yaml produces a working httpx-based client.