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

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

namestring · max 100 charsoptional

Customer-facing label shown in your dashboard.

metadataobjectoptional

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

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 · 23-29 · 30-39 · 40-plusAdult-only ranges by design. All values map to clearly-adult descriptors; under-18 is hard-blocked by moderation.
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 · largeOnly applies when gender=female. Ignored for male.

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.

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

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 sd_portrait (560×720) for fast / cheap output; pass hd_portrait for max detail (896×1152, ~30% slower). 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.

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.

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 output, within model determinism.

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 HD upgrade. Character portraits, mobile vertical, social.
sd_portrait 560 × 720 700 × 900 840 × 1080 Default. Faster than HD; slightly less micro-detail, identical face fidelity. Anime renders here single-pass.
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: 448×576 portrait (7:9). Fixed across all gens: the video model is tuned for this aspect ratio. For best results pass a portrait source image (sd_portrait or hd_portrait from /v1/images:generate). Square or landscape source images get center-cropped + resampled and may lose face / composition detail. If you generated the source via the character avatar (no generation_id), the avatar is already portrait, so no action needed.

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 image gen instead of the character avatar. Must reference one of your own completed image generations (returned from POST /v1/images:generate). The gen's output_url becomes the I2V first frame. Use this when you want to vary the starting pose / outfit / scene without creating a new character.

promptstring · max 2000 charsoptional

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). 16 fps in both cases. 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.

POSE KEYEXAMPLE ALIASES
doggy"doggy", "doggy style"
cowgirl"cowgirl", "riding on top"
reverse-cowgirl"reverse cowgirl"
missionary"missionary"
pov-oral"blowjob pov", "pov bj", "deepthroat", "blowjob" (default oral fallback)
bj-under-desk"under desk", "desk blowjob", "secretary blowjob"
squatting-bj"squatting blowjob", "squat bj"
blowjob-side"blowjob side", "side bj"
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"
fingering"fingering", "solo masturbation"
cum-in-mouth"cum in mouth", "mouthful cum"
cumshot-on-face"facial", "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.

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

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.

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.

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