# Xavira API reference Plug-and-play REST. Smart defaults: you pass `character_id` + `prompt`, we handle the rest. Base URL: `https://api.xavira.ai` · Docs: https://xavira.ai/docs · OpenAPI: https://xavira.ai/openapi.yaml > Prompts must be written in English. Other languages reach the image encoder > untranslated on a best-effort path and change what gets rendered. --- ## Quickstart Three steps from zero to a generated image: - Create an API key on your [Profile](https://xavira.ai/profile) page. - Create a persistent character via `POST /v1/characters` with a trait combo, and we'll generate the base avatar. - 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`): ```bash 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": "", "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](https://xavira.ai/profile) page. Use a separate key per environment (production / staging / CI). ```bash Authorization: Bearer xav_live_ ``` 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_ID | KIND | COST | BEST 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](#trait-enums) 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](#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](#scene-vocab) 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`](#sfw-images). 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](#characters-async) 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`](#images). > **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 | FIELD | VALUES | NOTES | | --- | --- | --- | | gender | female · male | Determines pronouns and gendered descriptors in the prompt. | | ethnicity | white · black · hispanic · middle-eastern · indian · east-asian · south-east-asian | Maps to natural-language descriptors (realistic) or skin-tone tags (anime). | | ageRange | 18-22 · 21-22 · 23-29 · 30-39 · 40-plus | Adult-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. | | hairLength | short · medium · long | — | | hairColor | black · brown · blonde · red · auburn · grey · white | — | | build | petite · slim · athletic · curvy · voluptuous | Adult body types only. Petite means "small adult", never "child-like". | | breastSize | small · medium · large · very-large · huge | Only applies when gender=female. Ignored for male. large is the middle of the scale, not the top. | | assSize | small · medium · large · very-large · huge | Optional, independent of build. Only applies when gender=female. large is the middle of the scale. | ### Response (201) ```json { "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. ```bash 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) ```json { "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](#trait-enums). 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. ```bash 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) ```json { "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](#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](#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](#resolution-presets). posestring · enumoptional Name the pose template yourself instead of letting us infer it from your prompt. Pass any key from [known pose aliases](#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](#sfw-images) for the approach that replaces it. (`sfw` on [`/v1/characters`](#characters-create) 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. ```json { "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. | RESOLUTION | BASE | FINAL @ 1.25x | FINAL @ 1.5x | NOTES | | --- | --- | --- | --- | --- | | 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) ```json { "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) ```json { "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](#status) 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](#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](#videos-webhook) 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": "", "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": "", "generation_id": "2e301acc-...", "prompt": "slow head turn, hair gently flowing" }' # → { "generation_id": "b83bc79c-...", "status": "pending", ... } ``` ### Response (202) ```json { "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) ```bash POST Headers: X-Xavira-Generation-Id: X-Xavira-Delivery-Id: 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](#webhook-verify). ## 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. ```json { "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_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: ```json { "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 KEY | EXAMPLE 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. - **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 `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 ```bash curl -X POST https://api.xavira.ai/v1/images:generate \ -H "Authorization: Bearer xav_live_" \ -H "Content-Type: application/json" \ -d '{ "character_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. _[afbeelding: SFW example: crew-neck knit sweater and jeans, full body]_ Crew-neck sweater + jeans, sunlit living room, full body. _[afbeelding: 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`](#images-generate) 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: ` 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](https://xavira.ai/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 `"."`. ### 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 - **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) ```json { "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. ```json { "error": { "code": "moderation_blocked", "message": "school setting", "details": { "tier": "ai", "category": "ai_flagged" }, "request_id": "01M113S6FJ2BDRX5N0PBFG1V5E" }, "request_id": "01M113S6FJ2BDRX5N0PBFG1V5E" } ``` | HTTP | ERROR.CODE | WHEN | | --- | --- | --- | | 400 | invalid_* | Body / param validation. Examples: invalid_character_id, invalid_prompt, invalid_traits. | | 401 | unauthorized | Missing or invalid API key. | | 402 | insufficient_credits | Balance too low. Response includes current balance + required cost in details. | | 404 | character_not_found | Unknown character_id, OR it belongs to another customer. We don't leak which. | | 405 | method_not_allowed | Wrong HTTP verb on an existing endpoint. | | 409 | idempotency_conflict | Idempotency-Key reused while the prior request is still pending or failed. | | 409 | character_pending | The character was created with `wait: false` and its avatar is still rendering. Poll its generation, then retry. | | 422 | moderation_blocked | Prompt or output flagged. Details include tier + category. No credit charged. | | 429 | rate_limited | Per-key per-minute cap exceeded. Honour the Retry-After header (always < 60 sec). See Rate limits. | | 500 | internal_error | DB transaction failed. No credit charged. Safe to retry. | | 502 | upstream_error | Upstream 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. | ENDPOINT | CAP / 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](mailto: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](https://www.postman.com/), [Insomnia](https://insomnia.rest/), [openapi-generator](https://github.com/OpenAPITools/openapi-generator), or any code-gen tool of your choice. [Download openapi.yaml](https://xavira.ai/openapi.yaml) [View raw](https://xavira.ai/openapi.yaml) ### 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](https://xavira.ai/xavira.postman_collection.json) 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](https://github.com/narrinai/xavira/tree/main/packages/sdk-js). ### Python SDK On the roadmap. In the meantime, `openapi-generator` + `openapi.yaml` produces a working `httpx`-based client.