API reference
Plug-and-play REST. Smart defaults: you pass character_id + prompt, we handle the rest.
Quickstart
Three steps from zero to a generated image:
- Create an API key on your Profile page.
- Create a persistent character via
POST /v1/characterswith a trait combo, and we'll generate the base avatar. - Generate as many follow-up images as you want via
POST /v1/images:generateusing the returnedcharacter_id.
End-to-end cURL (replace $XAVIRA_API_KEY):
curl -X POST https://api.xavira.ai/v1/characters \
-H "Authorization: Bearer $XAVIRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model_id": "realistic-sharp-v1",
"traits": {
"gender": "female",
"ethnicity": "east-asian",
"ageRange": "23-29",
"hairLength": "long",
"hairColor": "black",
"build": "slim",
"breastSize": "medium"
},
"name": "Sora"
}'
# → returns character_id
curl -X POST https://api.xavira.ai/v1/images:generate \
-H "Authorization: Bearer $XAVIRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"character_id": "<from above>",
"prompt": "sitting in a sunlit kitchen, casual sweater, candid"
}'
Authentication
Every request requires an API key in the Authorization header. Mint keys on your Profile page. Use a separate key per environment (production / staging / CI).
Authorization: Bearer xav_live_<your-key>
Keys are hashed at rest. The plaintext is shown once at creation time; lose it and you'll need to revoke + regenerate.
Models
Three production models, one API. Pick the model_id you want, and we handle the rest.
duration param. Async, webhook + poll.
Endpoints
All endpoints accept JSON, all return JSON. Base URL: https://api.xavira.ai.
POST /v1/characters
Create a persistent character by passing a closed-enum trait combo. We generate the base avatar and remember the identity for every subsequent generation against this character.
Body
One of realistic-sharp-v1, anime-pure-v1.
Closed-enum traits: see the trait enum table below. Closed enums prevent prompt-injection via trait input and give predictable output across customers using the same combo.
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).
Customer-facing label shown in your dashboard.
Arbitrary JSON for your own bookkeeping (internal ID, tag, etc).
Avatars are always max quality. Character creation always renders athd_portrait(896 × 1152) withhires_fix: "1.5x"on. The base avatar is a one-time investment that conditions every subsequent generation for this character, with no reason to expose a "downgrade" knob. Per-genresolutionandhires_fixare still customer-settable on/v1/images:generate.
No reference-image uploads. Characters are created exclusively from trait combos. Uploading photos for adult-content generation creates legal exposure (image-based sexual abuse / NCII) we deliberately don't accept on the platform. All identity comes from prompts and trait selections.
Trait enum table
female · maleDetermines pronouns and gendered descriptors in the prompt.white · black · hispanic · middle-eastern · indian · east-asian · south-east-asianMaps to natural-language descriptors (realistic) or skin-tone tags (anime).18-22 · 23-29 · 30-39 · 40-plusAdult-only ranges by design. All values map to clearly-adult descriptors; under-18 is hard-blocked by moderation.short · medium · long—black · brown · blonde · red · auburn · grey · white—petite · slim · athletic · curvy · voluptuousAdult body types only. Petite means "small adult", never "child-like".small · medium · 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
Returned from POST /v1/characters. The model + face are inherited from the character.
Free-text. Describe the scene / pose / outfit / lighting; we already encode the character identity.
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.
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.
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.
Appended to our house negatives (we already block deformed hands, watermarks, etc). Use only for additional aesthetic exclusions.
For reproducibility. Same seed + same prompt + same character = same 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.
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.
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
Must be an image-model character. Anime characters work too. Required even when you pass generation_id; used for ownership + tracking.
Animate a specific prior 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.
Describe the motion you want, not the scene. E.g. "slow head turn, hair gently flowing". If omitted, the API uses a safe anti-drift default that keeps the character + scene anchored ("same scene, subtle motion, gentle breathing"). Power users can still pass their own motion prompt for full control.
Same idea as for images.
Reproducible motion.
Clip length. "5s" (default, 5 credits) or "10s" (10 credits). 16 fps in both cases. 10s gens take roughly 2× the wall-clock of 5s on the same hardware.
We POST a JSON payload here when the gen completes. See webhook payload below. 5 retries with exponential backoff (1/2/4/8/16 min) before abandoning.
Two-step example: animate a generated image
# 1. Generate an image you like
curl -X POST https://api.xavira.ai/v1/images:generate \
-H "Authorization: Bearer $XAVIRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"character_id": "<char-id>",
"prompt": "leaning against a rooftop railing at blue hour, silk slip dress"
}'
# → { "generation_id": "2e301acc-...", "output_url": "https://...", ... }
# 2. Animate that exact image
curl -X POST https://api.xavira.ai/v1/videos:generate \
-H "Authorization: Bearer $XAVIRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"character_id": "<char-id>",
"generation_id": "2e301acc-...",
"prompt": "slow head turn, hair gently flowing"
}'
# → { "generation_id": "b83bc79c-...", "status": "pending", ... }
Response (202)
{
"generation_id": "b83bc79c-576b-442e-aed8-af8b7fcd43e4",
"kind": "video",
"status": "pending",
"cost_credits": 5,
"poll_url": "/v1/generations/b83bc79c-576b-442e-aed8-af8b7fcd43e4"
}
Webhook payload (POST to your callback_url)
POST <callback_url>
Headers:
X-Xavira-Generation-Id: <uuid>
X-Xavira-Delivery-Id: <uuid>
X-Xavira-Attempt: 1
X-Xavira-Signature: t=1779350400,v1=a3f4...e9b # HMAC-SHA256, see below
Body:
{
"type": "video.completed",
"generation_id": "b83bc79c-...",
"character_id": "151fccbd-...",
"status": "completed", // or "failed"
"output_url": "https://pub-...r2.dev/.../video.mp4",
"gen_time_ms": 83582,
"wall_time_ms": 311,
"cost_credits": 5,
"error_message": null
}
Your endpoint should respond with a 2xx within 5 seconds. Any other status (or timeout) counts as failure; we'll retry with exponential backoff. After 5 failures the delivery is marked abandoned; you can still pull the generation via the poll endpoint. Always verify the signature before trusting the payload; see Webhook verification.
GET /v1/generations/:id
Poll a generation. Lazy completion: if status is still pending, we check the upstream worker and mutate the DB on terminal state (idempotent, safe to poll concurrently). On FAILED the credits are automatically refunded.
{
"generation_id": "b83bc79c-576b-442e-aed8-af8b7fcd43e4",
"kind": "video",
"status": "completed",
"output_url": "https://pub-...r2.dev/.../video.mp4",
"gen_time_ms": 83582,
"wall_time_ms": 311,
"cost_credits": 5
}
For sync image gens this is just a DB read. Suggested poll cadence for videos: every 5-10 seconds, max wall-clock budget ~3 minutes (cold start + ~80s gen).
Prompt enhancement (default on)
Every POST /v1/images:generate call runs your prompt through a server-side enhancement layer before it reaches the model. This is on by default; set raw_prompt: true to opt out.
What enhancement does
- Pose alias matching. Short labels like
"blowjob pov","doggy","reverse cowgirl"are matched against a curated pose library and swapped for the fully-expanded prompt, with camera angles, weighted tokens, and anatomy anchors. Without this, a short prompt like"blowjob pov"gives the model too little signal and it defaults to a portrait. - Identity anchor. The character's stored traits (
age+ethnicity+hairLength+hairColor+breastSize) are formatted into a natural-language phrase and prepended, e.g."26 year old Hispanic Latina woman, tan brown skin, long brown hair, medium breasts, ". Improves consistency across generations. - Quality prefix. A fixed string is prepended:
"RAW photo, DSLR, 8k uhd, film grain, natural skin texture, visible pores, ...", counteracting the model's AI-stockphoto bias and produces noticeably more "real photo" output. - Negative additions. A fixed
FACEID_NEGATIVE_PROMPTcovering common artifacts (mirror reflections, ghost breasts, watermarks, deformed anatomy, etc.) is appended to yournegative_prompt_appendon top of the model's own house negatives.
Transparency: the enhanced_prompt response field
The 201 response includes:
{
"generation_id": "...",
"output_url": "...",
"enhanced_prompt": "RAW photo, DSLR, 8k uhd, ... 26 year old Hispanic Latina woman, ... POV from above, deepthroat, ...",
"matched_pose": "pov-oral",
...
}
matched_pose is null when your input didn't hit a known alias; in that case enhanced_prompt is just QUALITY_PREFIX + traits + your_prompt_verbatim. Suppressed entirely when raw_prompt: true since nothing was modified.
Known pose aliases
Common phrasings get matched (case-insensitive, substring search). Sample aliases below; see matched_pose in the response to confirm a match.
Building custom poses (no alias match)
If your prompt doesn't hit any alias, we still apply the quality prefix + traits + negatives, but your prompt itself ships verbatim as the scene description. To get good results for a custom pose:
- Be specific: camera angle + body position + key anatomy details + lighting. E.g.
"yoga child's pose, viewed from behind, soft natural morning light, hands stretched forward, ass slightly raised"works better than"yoga pose". - Use weighted tokens for the elements you want emphasized:
"(arched back:1.4)","(eyes locked on camera:1.3)". - Check
enhanced_promptin the response to see exactly what reached the model. Iterate from there. - For tightest control, pass
raw_prompt: true: you take full ownership of the prompt; only the model's house-negatives stack on top.
When to use raw_prompt: true
- You already include your own quality cues and weighted tokens.
- You're building your own prompt-construction layer and want our backend to stay out of it.
- You're A/B-ing alternatives: disable enhancement, compare against the default.
Scene vocabulary
The closed-enum traits cover identity (gender · ethnicity · age · hair · build). Everything else (eye color, freckles, makeup, tattoos, accessories, lighting, mood, outfit details, pose, location) lives in the free-text scene field (when you create the character) and in prompt (when you generate). That keeps the API surface small while letting you express anything natural language can.
This isn't a closed list. The model understands English. Here's a starter kit of phrases that consistently work; mix and match per gen.
Eye color & facial details
piercing blue eyes·warm brown eyes·green eyes·hazel eyes·grey eyes·amber eyeslight freckles across the nose·dense freckles·cheekbone frecklessmall mole above lip·beauty mark on cheek·cleft chin·dimplessoft jawline·strong jawline·high cheekbones·full lips·natural lipsglasses·cat-eye glasses·round wire glasses·aviator sunglasses
Makeup
natural no-makeup look·subtle makeup·nude lipstickwinged eyeliner·smokey eye·red lipstick·matte burgundy lipstickglossy lips·blush, freshly applied·bronzed cheeks
Tattoos & piercings
small floral tattoo on forearm·delicate line tattoos on arms·full sleeve tattooshoulder tattoo·collarbone tattoo·back tattoo, partially visiblenose stud·septum piercing·helix piercing·multiple ear piercings
Outfit & styling
- Casual:
oversized hoodie·cropped sweatshirt·vintage band tee·jeans and white sneakers - Workwear:
tailored blazer·linen button-down·pencil skirt·satin blouse - Eveningwear:
little black dress·silk slip dress·satin gown·backless top - Lingerie:
lace bralette·silk robe·black bodysuit·matching lingerie set - Hair styling:
wet hair·hair tied in a low bun·messy bun·side part·curtain bangs·blown-out hair
Lighting
- Natural:
golden hour·blue hour·soft morning light·overcast diffuse light·harsh midday sun·backlit by window - Studio:
three-point lighting·softbox key light·rim light·butterfly lighting - Cinematic:
moody low-key lighting·neon-lit night scene·candlelight·fire-lit warm tones·chiaroscuro
Mood / atmosphere
candid·posed editorial·spontaneous laughter·contemplative·flirty glance·seductive over-the-shoulder lookwarm and inviting·melancholic·energetic·intimate·powerful·vulnerable
Composition & framing
close-up portrait·medium shot·full body shot·three-quarter view·profileshallow depth of field·bokeh background·sharp focus throughoutshot on 35mm·shot on Hasselblad·iPhone selfie(each gives a distinct look)
Location / setting
- Indoor:
sunlit kitchen·industrial loft·cozy bedroom·luxury hotel suite·art gallery·vintage diner booth - Outdoor:
foggy coastline·autumn forest path·rooftop at dusk·desert at golden hour·cobblestone street in Paris - Time of day matters: always specify (
morning·afternoon·dusk·night): the model commits to a coherent lighting setup.
For anime-pure-v1: same idea, tag-style
Same vocabulary, but as comma-separated tags rather than prose. Examples:
blue eyes·heterochromia·freckles·mole under eye·fangtwintails·ponytail·messy hair·hair ornament·hair ribbonschool uniform alternative·maid outfit·kimono·casual·swimsuitlooking at viewer·looking away·upper body·cowboy shot·from belowsunset·night sky·cherry blossoms·indoor·outdoors
For video-v1 (video): describe motion
slow head turn·gentle smile spreading·hair flowing in breeze·eyes blinking·looking up to camerasips coffee·brushes hair behind ear·laughs softly·turns to look over shoulder
Heuristic. Two to four short comma-separated clauses beats one long sentence. The model treats prompts like a sequence of weighted features; long sentences dilute each feature's weight.
Prompt tips
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.parseoutput. Re-stringifying produces different key order → signature mismatch. - Use constant-time comparison (
timingSafeEqual/hmac.compare_digest). Equality with==leaks the secret via timing side-channel. - Reject old timestamps (5-min window). Without that, an attacker who once captured a webhook can replay it forever.
- Rotate on suspected leak via the Profile page: old signatures stop verifying immediately.
Moderation
Three-tier moderation runs on every gen. Adult content is allowed by design: the API is built for it. We block exclusively for:
- Minors: any age cue under 18, child-coded features, school settings, loli/shota.
- Non-consent: drugged/unconscious, forced acts, "against will" framing.
- Bestiality, incest involving identifiable family relations, extreme sexual violence/gore.
Three tiers
- Tier 1, rule-based: ~150 lexical patterns. Runs in <1ms, fail-fast on obvious matches.
- Tier 2, text classifier: a fast third-party text classifier. Catches paraphrasing the rules miss.
- Tier 3, output vision classifier: a third-party vision classifier checks the generated image (realistic models only, anime classifiers hallucinate on cartoons). If flagged: image is deleted from R2, no credit charged.
Block response (422)
{
"error": {
"code": "moderation_blocked",
"message": "Blocked: prompt contains restricted content (category=minors)",
"details": { "tier": "rule", "category": "minors" }
}
}
When tier=ai or tier=output the category is free-form. Surface the message to your end-user verbatim.
Errors
Every error follows the same shape: { "error": { "code": "...", "message": "...", "details"?: {...} } }. Match on error.code, not on the message.
invalid_character_id, invalid_prompt, invalid_traits.character_id, OR it belongs to another customer. We don't leak which.Retry-After header (always < 60 sec). See Rate limits.Rate limits
Per-API-key, fixed 1-minute windows. Each key has independent counters; using a separate key per environment is the recommended pattern. The caps below are the Starter / Builder baseline; Scale doubles them (2×) and Volume quadruples them (4×). Enterprise caps are negotiable.
Response headers
Every gen response carries rate-limit headers so you can back off gracefully:
X-RateLimit-Limit: 60 # cap for this endpoint X-RateLimit-Remaining: 47 # calls left in the current window X-RateLimit-Reset: 1779350400 # epoch seconds when window resets
When you hit the limit
HTTP 429 rate_limited. Honour the Retry-After header (always < 60 seconds, that's the window size).
HTTP/1.1 429 Too Many Requests
Retry-After: 23
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1779350423
{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded — 60/min for /v1/images:generate. Retry in 23s."
}
}
Need more? Email hello@xavira.ai. We'd rather lift your cap than have you build retry queues. Enterprise plan offers per-key custom caps.
Spec & SDKs
Machine-readable spec + a Postman collection so you can poke at the API without writing code.
OpenAPI 3.1 spec
Full schema for every endpoint, request body, response shape, and error code. Import into Postman, Insomnia, openapi-generator, or any code-gen tool of your choice.
Download openapi.yaml View raw
Postman collection
Pre-wired collection with bearer auth, variable extraction (response character_id auto-saves to a collection variable, then the image-gen request references it), and idempotency-key auto-generation via {{$guid}}.
Set the apiKey collection variable before running. The collection runs Status → Create character → Generate image in that order; later requests pick up {{characterId}} automatically.
JavaScript / TypeScript SDK
Official Node.js SDK with typed request + response shapes, HMAC webhook verification, rate-limit-aware errors, and a pollUntilComplete helper for async video gens.
npm install @xavira/sdk
import { Xavira } from "@xavira/sdk";
const xavira = new Xavira({ apiKey: process.env.XAVIRA_API_KEY! });
const character = await xavira.characters.create({
model_id: "realistic-sharp-v1",
traits: { gender: "female", ethnicity: "east-asian", ageRange: "23-29",
hairLength: "long", hairColor: "black", build: "slim",
breastSize: "medium" },
});
const image = await xavira.images.generate({
character_id: character.character_id,
prompt: "sitting in a sunlit kitchen, casual sweater, candid portrait",
});
console.log(image.output_url);
Source + full README on GitHub.
Python SDK
On the roadmap. In the meantime, openapi-generator + openapi.yaml produces a working httpx-based client.