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:

  1. Create an API key on your Profile page.
  2. Create a persistent character via POST /v1/characters with a trait combo, and we'll generate the base avatar.
  3. Generate as many follow-up images as you want via POST /v1/images:generate using the returned character_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.

MODEL_IDKINDCOSTBEST FOR
realistic-sharp-v1 image 1 credit Photorealistic portraits with consistent character identity across generations.
anime-pure-v1 image 1 credit Anime / stylized illustration with consistent traits across generations.
video-v1 video 5 / 10 credits Motion clip from any character avatar. 5s (5 credits) or 10s (10 credits) via 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

model_idstring · enumrequired

One of realistic-sharp-v1, anime-pure-v1.

traitsobjectrequired

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.

scenestring · max 1000 charsoptional

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).

sfwbooleanoptional, default false

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.

namestring · max 100 charsoptional

Customer-facing label shown in your dashboard.

metadataobjectoptional

Arbitrary JSON for your own bookkeeping (internal ID, tag, etc).

waitbooleanoptional, default true

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.

callback_urlstring · https URLoptional

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 at hd_portrait (896 × 1152) with hires_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-gen resolution and hires_fix are 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

FIELDVALUESNOTES
genderfemale · maleDetermines pronouns and gendered descriptors in the prompt.
ethnicitywhite · black · hispanic · middle-eastern · indian · east-asian · south-east-asianMaps to natural-language descriptors (realistic) or skin-tone tags (anime).
ageRange18-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.
hairLengthshort · medium · long
hairColorblack · brown · blonde · red · auburn · grey · white
buildpetite · slim · athletic · curvy · voluptuousAdult body types only. Petite means "small adult", never "child-like".
breastSizesmall · medium · large · very-large · hugeOnly applies when gender=female. Ignored for male. large is the middle of the scale, not the top.
assSizesmall · 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:generate and /v1/videos:generate answer 409 character_pending while 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 in status: "failed" with the reason in error_message, and the character retired. Retrying with the same Idempotency-Key is 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

traitsobjectoptional

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.

namestring · max 100 charsoptional

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

character_iduuidrequired

Returned from POST /v1/characters. The model + face are inherited from the character.

promptstring · max 2000 chars (5000 on Volume)required

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.

resolutionstring · enumoptional

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.

hires_fixstring · enumoptional

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.

posestring · enumoptional

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.

raw_promptbooleanoptional, default false

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.

sfwremovedaccepted, ignored

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.)

clothed_posebooleanoptional, default false

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.

clothed_confidencebooleanoptional, default false

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.

negative_prompt_appendstring · max 1000 charsoptional

Appended to our house negatives (we already block deformed hands, watermarks, etc). Use only for additional aesthetic exclusions.

seednon-negative integeroptional

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.

RESOLUTIONBASEFINAL @ 1.25xFINAL @ 1.5xNOTES
hd_portrait 896 × 1152 1120 × 1440 1344 × 1728 Default. Most facial detail (teeth, eyes, skin). Character portraits, mobile vertical, social.
sd_portrait 560 × 720 700 × 900 840 × 1080 Faster, lighter render; less micro-detail in the face. Same credit cost as HD.
hd_landscape 1152 × 896 1440 × 1120 1728 × 1344 Wide framing, scene-heavy shots.
sd_landscape 864 × 672 1080 × 840 1296 × 1008 Faster landscape.
hd_square 1024 × 1024 1280 × 1280 1536 × 1536 Social-feed friendly.
sd_square 768 × 768 960 × 960 1152 × 1152 Faster square.

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.

Output format: your source frame's aspect ratio, at a 448×576 pixel budget. Until 2026-08-18 this was a hard 448×576 on every gen, which was wrong: the workflow has no resize step, so a first frame that was not already 7:9 came out stretched rather than cropped. Anime avatars render at 896×1152 and were therefore stretched on every clip. We now read the ratio off your first frame and match it, searching the grid the model requires (both sides divisible by 16) while holding the pixel count within 15% of the budget, so render time and credit cost are unchanged. Nothing is cropped and nothing is stretched. If you were pre-cropping sources to 7:9 to work around this, you can stop.

Body

character_iduuidrequired

Must be an image-model character. Anime characters work too. Required even when you pass generation_id; used for ownership + tracking.

generation_iduuidoptional

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.

promptstring · max 2000 charsoptional

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.

negative_prompt_appendstringoptional

Same idea as for images.

seedintegeroptional

Reproducible motion.

durationstring · enumoptional

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.

callback_urlhttps URLoptional

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

  1. 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.
  2. 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.
  3. 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.
  4. Negative additions. A fixed FACEID_NEGATIVE_PROMPT covering common artifacts (mirror reflections, ghost breasts, watermarks, deformed anatomy, etc.) is appended to your negative_prompt_append on 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.

POSE KEYEXAMPLE ALIASES
doggy"doggy", "doggy style"
cowgirl"cowgirl", "riding on top"
reverse-cowgirl"reverse cowgirl"
missionary"missionary"
pov-oral"blowjob pov", "pov bj", "blowjob" (default oral fallback)
deepthroat"deepthroat", "deep throat"
kneeling-bj"kneeling blowjob", "on her knees sucking"
bj-under-desk"under desk", "desk blowjob", "secretary blowjob"
face-fuck"face fuck", "throat fuck", "gagging"
spooning"spooning", "side by side"
holding-legs"holding legs", "legs held up"
on-all-fours"on all fours", "hands and knees", "ass up"
bent-over"bent over"
spread-ass"spread ass", "spread cheeks"
spread-pussy"spread pussy", "spread her pussy", "pussy spread open"
anal-toy"butt plug", "buttplug", "anal plug"
striptease"striptease", "strip tease", "stripping", "undressing"
fingering"fingering", "solo masturbation"
cum-in-mouth"cum in mouth", "mouthful cum"
cumshot-missionary"cumshot missionary", "cum on her tits", "cum on chest"
cumshot-on-face"facial cumshot", "cum on face"
selfie"selfie", "phone selfie"
standing"standing nude", "standing pose"
showing-breasts"showing tits", "presenting breasts"
flashing-tits"flashing tits", "lift shirt"
+ 10 male poses"hard cock", "stroking cock", "shower", "abs flex", "mirror selfie", "back muscles", "masturbating", "lying down", "standing nude man", "bed spread"

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_prompt in 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.

  1. 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.
  2. 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.
  3. 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 swimwear and bikini, 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.

SFW example: crew-neck knit sweater and jeans, full body
Crew-neck sweater + jeans, sunlit living room, full body.
SFW example: chunky turtleneck sweater in a cafe
Chunky turtleneck + trousers, cafe table, morning light.

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 in negative_prompt_append, never in prompt.

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.

The 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 eyes
  • light freckles across the nose · dense freckles · cheekbone freckles
  • small mole above lip · beauty mark on cheek · cleft chin · dimples
  • soft jawline · strong jawline · high cheekbones · full lips · natural lips
  • glasses · cat-eye glasses · round wire glasses · aviator sunglasses

Makeup

  • natural no-makeup look · subtle makeup · nude lipstick
  • winged eyeliner · smokey eye · red lipstick · matte burgundy lipstick
  • glossy lips · blush, freshly applied · bronzed cheeks

Tattoos & piercings

  • small floral tattoo on forearm · delicate line tattoos on arms · full sleeve tattoo
  • shoulder tattoo · collarbone tattoo · back tattoo, partially visible
  • nose 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 look
  • warm and inviting · melancholic · energetic · intimate · powerful · vulnerable

Composition & framing

  • close-up portrait · medium shot · full body shot · three-quarter view · profile
  • shallow depth of field · bokeh background · sharp focus throughout
  • shot 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 · fang
  • twintails · ponytail · messy hair · hair ornament · hair ribbon
  • school uniform alternative · maid outfit · kimono · casual · swimsuit
  • looking at viewer · looking away · upper body · cowboy shot · from below
  • sunset · 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 camera
  • sips 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 seed per request. That is what gives you variety across a set.
  • Everything you want to avoid goes in negative_prompt_append, never in prompt. Words like plastic, CGI and AI-generated in 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: true keeps the composition and the act while keeping the character dressed, including on oral poses.
  • Check matched_pose in 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.parse output. 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

  1. Tier 1, rule-based: ~150 lexical patterns. Runs in <1ms, fail-fast on obvious matches.
  2. Tier 2, text classifier: a fast third-party text classifier. Catches paraphrasing the rules miss.
  3. 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"
}
HTTPERROR.CODEWHEN
400invalid_*Body / param validation. Examples: invalid_character_id, invalid_prompt, invalid_traits.
401unauthorizedMissing or invalid API key.
402insufficient_creditsBalance too low. Response includes current balance + required cost in details.
404character_not_foundUnknown character_id, OR it belongs to another customer. We don't leak which.
405method_not_allowedWrong HTTP verb on an existing endpoint.
409idempotency_conflictIdempotency-Key reused while the prior request is still pending or failed.
409character_pendingThe character was created with wait: false and its avatar is still rendering. Poll its generation, then retry.
422moderation_blockedPrompt or output flagged. Details include tier + category. No credit charged.
429rate_limitedPer-key per-minute cap exceeded. Honour the Retry-After header (always < 60 sec). See Rate limits.
500internal_errorDB transaction failed. No credit charged. Safe to retry.
502upstream_errorUpstream compute, storage, or moderation provider failed. No credit charged. Safe to retry.

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.

ENDPOINTCAP / MIN (BASE)NOTES
POST /v1/characters 30 Character creation is rare; cap stays generous. Scale 60 / Volume 120.
PATCH /v1/characters/:id 60 Trait edits only, nothing is rendered. Scale 120 / Volume 240.
POST /v1/images:generate 60 Matches warm-worker throughput. Scale 120 / Volume 240.
POST /v1/videos:generate 5 Video gens cost 5 credits + ~80s GPU each; bursts are expensive.
GET /v1/generations/:id unlimited Polling-friendly. DB read + optional upstream status check.

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}}.

Download Postman collection

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.