# LetzAI API Instructions

LetzAI API offers image and video creation, as well as image manipulation endpoints to build AI Image Generation and Video apps.
This document provides instructions for AI models and code builders to interact with the LetzAI API.

## Base URL

BaseURL: https://api.letz.ai

Swagger: https://api.letz.ai/doc

Auth: Bearer YOUR_API_KEY (Can be created on: www.letz.ai/subscription)

Note: This is the PUBLIC API. private-api.letz.ai is the internal web-app API and uses
different route names (/upscales, /image_completions, /video-edits) which 404 here.

Note: Request bodies are validated strictly - unknown fields return 400. There is no
negativePrompt, seed or aspectRatio on POST /images; use width/height for aspect ratio.

## Status Values

-   Images: new|generating|ready|hidden|failed|interrupted|not_allowed
-   Videos: new|generating|ready|saved|failed|interrupted
-   Image Edits: new|generating|ready|saved|failed|interrupted
-   Upscales: new|generating|ready|failed
-   Models: new|pending|training|finished|available|failed
-   There is no "in progress" status; in-flight jobs report "generating"
-   Terminal success: ready (and saved for videos/edits). Terminal failure: failed|interrupted|not_allowed (see statusDetail)

## Endpoints

### Images

Main Endpoints for Image Inference, image publication and job interruption

-   POST /images
    Body: {prompt:string(req),width:int(480-2160,1600),height:int(480-2160,1600),quality:int(1-6,2),creativity:int(1-6,2),hasWatermark:bool(true),systemVersion:int(2|3),mode:string(resolution tier),hideFromUserProfile:bool(false),baseModel:string,webhookUrl:string,organizationId:string(uuid)}
    Note: mode is the RESOLUTION TIER (default|1k|2k|hd|4k), allowed values differ per model - see table below
    Note: baseModel options - "gemini-3-pro-image" (Nano Banana Pro), "nbf-inferencesh" (Nano Banana 2), "nano-banana-2-lite" (Nano Banana 2 Lite), "seedream-5-0-pro" (Seedream 5 Pro), "seedream-4-5-251128" (Seedream 4.5), "flux2" (Flux 2), "gpt-image-2" (GPT Image 2), "wan-2-7-image-pro" (WAN 2.7 Image Pro)
    Note: modes+pricing - Nano Banana Pro (default:80, 2k:160, 4k:240), Nano Banana 2 (default:40, 2k:80, 4k:160), Nano Banana 2 Lite (default:20), Seedream 5 Pro (2k:80, 4k:160), Seedream 4.5 (2k:80, 4k:160), Flux 2 (1k:60, hd:120), GPT Image 2 (1k:160, hd:240, 4k:480), WAN 2.7 (1k:10, 2k:10)
    Note: Live catalogue with current models + pricing: https://letz.ai/docs/models.json
    Resp: 201, {id:string,prompt:string,baseModel:string,status:string,progress:int(0-100),previewImage:string(base64),imageVersions:object,hasWatermark:bool,privacy:string(private|public|licensed),createdAt:string(iso)}

-   GET /images/{id}
    Path: id:string(uuid,req)
    Resp: 200, {id:string,prompt:string,status:string,statusDetail:string,progress:int(0-100),previewImage:string(base64),imageVersions:object,generationSettings:object,models:array,privacy:string,createdAt:string(iso)}
    Note: Poll every 3s until terminal status

-   GET /images
    Query: {page:int(1),limit:int(1-50,10),sortBy:string(createdAt|likes|regenerations),sortOrder:string(ASC|DESC),userId:string,username:string,privacy:string,systemVersion:int,isHot:bool,includeNsfw:bool(false),onlyFollowing:bool,modelIds:array,status:string}
    Resp: 200, [{id:string,prompt:string,status:string,progress:int,previewImage:string,...}]
    Note: Pagination in response headers X-Total-Count, X-Current-Page, X-Per-Page, X-Total-Pages

-   PUT /images/{id}/interruption
    Path: id:string(uuid,req)
    Resp: 204 (success), 400 (error)
    Note: Stops image generation

-   PUT /images/{id}/privacy
    Path: id:string(uuid,req)
    Body: {privacy:string(public|private|licensed,private)}
    Resp: 204 (success), 400 (error)
    Note: Public images visible on LetzAI feed

-   PUT /images/{id}/prompt-privacy
    Path: id:string(uuid,req)
    Resp: 204 (success), 400 (error)
    Note: Hides/shows the prompt on public listings

### Models

Endpoints to retrieve, train and manage AI Models.
Models can be tagged in prompts using @ character, e.g. "@modelname on the beach"

-   GET /models
    Query: {page:int(1),limit:int(10),sortBy:string(createdAt|usages),sortOrder:string(ASC|DESC),search:string,name:string,userId:string,username:string,class:string(person|object|style),type:string,description:string,privacy:string(public|private|licensed),systemVersion:int,isActive:bool,status:string}
    Resp: 200, [{id:string,name:string,class:string,status:string,usages:int,thumbnail:string,createdAt:string(iso)}]
    Note: Pagination in response headers X-Total-Count, X-Current-Page, X-Per-Page, X-Total-Pages

-   GET /models/{id}
    Path: id:string(uuid,req)
    Resp: 200, {id:string,name:string,class:string,type:string,status:string,progress:int,privacy:string,usages:int,thumbnail:string,versions:array,createdAt:string(iso)}

-   POST /models
    Body: {name:string(req,alphanumeric+_+.,max 50),class:string(person|style|object,req),privacy:string(public|private|licensed),type:string,trainingDataUrls:[string](1-50 public URLs),trainingMode:string(default|slow),settings:[string](NO_ADULT_CONTENT|NO_VIOLENCE|...),description:string,website:string,webhookUrl:string,organizationId:string(uuid)}
    Note: Enterprise plans. Training starts automatically; poll GET /models/{id} until status=available
    Note: name becomes the @tag and cannot be changed later
    Resp: 201, {id:string,name:string,class:string,status:string,...}

-   PATCH /models/{id}
    Body: {privacy:string,type:string,description:string,website:string,settings:[string]}
    Note: name cannot be updated

-   DELETE /models/{id}

-   PUT /models/{id}/thumbnail
    Body: {imageCompletionId:string(uuid)}

### Upscaling

Increases the size of an image and makes it slightly more detailed

-   POST /upscale
    Note: PUBLIC route is /upscale (singular). /upscales is the internal web-app route and 404s here.
    Body: {imageId:string(uuid)|xor|imageUrl:string(req),mode:string(max 32 chars,default),size:int(2-12),strength:int(1-5,1),prompt:string,webhookUrl:string,organizationId:string(uuid)}
    Note: mode selects the upscaler: nano-banana-pro=Gemini 3 Pro, nano-banana-2=Gemini 3.1 Flash, gpt-image-2=OpenAI GPT Image 2, pruna=Pruna P-Image-Upscale, default=LetzAI in-house
    Note: size is the output tier: 4=1K, 8=2K, 12=4K (pruna: 4=1MP, 8=4MP, 12=16MP)
    Note: There is no assetId/assetUrl/type on the public upscale route - upload via /user-assets and pass the returned imageUrl
    Note: Pricing - nano-banana-pro (80/160/240), nano-banana-2 (50/100/150), gpt-image-2 (160/240/480), pruna (20/40/80), default (20)
    Note: There is no generationSettings object on the public API - mode/size/strength are top-level
    Resp: 201, {id:string,status:string,progress:int,mode:string,size:int,imageVersions:object,...}
    Note: Poll /upscale/{id} every 3s until terminal status

-   GET /upscale/{id}
    Path: id:string(uuid,req)
    Resp: 200, {id:string,status:string,progress:int,imageVersions:object,imageUrl:string,imageCompletionId:string}
    Note: Poll every 3s until terminal status

-   GET /upscale
    Query: {page:int(1),limit:int(1-50,10),sortBy:string,sortOrder:string(ASC|DESC),userId:string,username:string,imageId:string,onlyFollowing:bool,status:string}

-   DELETE /upscale/{id}

### Image Editing

Allows to edit images using Context Editing (primary mode) or Skin Fix.
To pass reference images, use inputImageUrls[] on POST (max 9 images).
Note: Inpainting (mode: "in") and Outpainting (mode: "out") are deprecated - use Context Editing instead.

-   POST /image-edits
    Body: {mode:string(context|skin,req),prompt:string,inputImageUrls:[string],imageUrl:string,originalImageCompletionId:string(uuid),originalImageCompletionIds:[string],imageCompletionsCount:int(1-5,1),width:int,height:int,mask:string,settings:object,organizationId:string(uuid),webhookUrl:string,hidePrompt:bool(false),baseModel:string}
    Settings for context editing: {resolution:string(2k|4k,2k),aspect_ratio:string(1:1|16:9|9:16|4:3|3:4|21:9|9:21),model:string}
    Note: resolution "2k" = HD, "4k" = 4K
    Note: baseModel/model options are the same identifiers as POST /images (gemini-3-pro-image, nbf-inferencesh, nano-banana-2-lite, seedream-5-0-pro, seedream-4-5-251128, flux2, gpt-image-2, wan-2-7-image-pro)
    Note: Prefer the top-level baseModel - queue routing reads it; settings.model is forwarded to the worker. Set both to the same value.
    Note: Editing Pricing - Nano Banana Pro (80/160/240), Nano Banana 2 (50/100/150), Nano Banana 2 Lite (25), Seedream 5 Pro (80/160), Seedream 4.5 (80/160), Flux 2 (60/120), GPT Image 2 (160/240/480), WAN 2.7 (10)
    Example settings: {aspect_ratio:"16:9",model:"gemini-3-pro-image",resolution:"2k"}
    Resp: 201, {id:string,originalImageCompletion:object,generatedImageCompletion:object,imageCompletionChoices:array,mode:string,status:string,prompt:string,settings:object}
    Note: Poll /image-edits/{id} every 3s until terminal status
    Note: Either imageUrl, inputImageUrls, originalImageCompletionId or originalImageCompletionIds is required
    Note: Result is at generatedImageCompletion.imageVersions.original

-   GET /image-edits/{id}
    Path: id:string(uuid,req)
    Resp: 200, {id:string,originalImageCompletion:object,generatedImageCompletion:object,mode:string,status:string,statusDetail:string,progress:int,settings:object}
    Note: Poll every 3s until terminal status

-   GET /image-edits
    Query: {page:int(1),limit:int(20),sortBy:string(createdAt|status),sortOrder:string(ASC|DESC),status:string(new|generating|ready|saved|failed|interrupted)}
    Resp: 200, [{id:string,mode:string,status:string,prompt:string,...}]
    Note: Pagination in response headers X-Total-Count, X-Current-Page, X-Per-Page, X-Total-Pages

-   GET /image-edits/{imageCompletionId}/mask
    Path: imageCompletionId:string(uuid,req)
    Note: Returns the mask used for a legacy inpainting edit

### Videos

Endpoints to create videos. Supports text-to-video, image-to-video, and multi-image first/last frame animations.

-   POST /videos
    Body: {prompt:string(req),originalImageCompletionId:string(uuid),imageUrl:string,imageUrls:[string],prompts:[object],width:int,height:int,resolution:int,baseModel:string,settings:object,organizationId:string(uuid),webhookUrl:string,hidePrompt:bool(false)}
    Settings: {mode:string(video-*, see below),duration:int,withSound:bool,highQuality:bool,resolution:string(480p|720p|1080p|4k)}
    Note: Image input is OPTIONAL - omit originalImageCompletionId/imageUrl/imageUrls for text-to-video
    Note: settings.mode uses the video- prefixed key, not the bare alias
    Note: Video models - video-veo31 (Google Veo 3.1, 8s fixed, 1500cr, x2 audio, x2 1080p), video-kling3 (Kling V3, 3-15s, 150cr / 300cr with audio, multi-shot via prompts[]), video-kling26 (Kling V2.6, 5 or 10s, 150cr, x2 audio, x2 1080p), video-wan25 (WAN 2.5, 5 or 10s, 110cr, x2 1080p, image-to-video only), video-seedance2 (Seedance 2.0, 4-15s, per-second 105@480p/210@720p/500@1080p/1000@4k, audio always on), video-seedance2-enterprise (same, requires organizationId), video-gemini-omni (Gemini Omni Flash, 3-10s, 100cr/s, audio always on)
    Note: Live catalogue with current models + pricing: https://letz.ai/docs/models.json
    Note: imageUrls accepts 2 images for first/last frame animations: [0]=first frame, [1]=last frame (video-veo31, video-kling3, seedance)
    Note: prompts is a top-level array of {prompt,duration} objects for video-kling3 multi-shot (min 3s per shot, 15s total)
    Resp: 201, {id:string,status:string,progress:int(0-100),prompt:string,width:int,height:int,settings:object,privacy:string,createdAt:string(iso)}
    Note: Poll /videos/{id} every 2-3s until terminal status

-   GET /videos/{id}
    Path: id:string(uuid,req)
    Resp: 200, {id:string,status:string,statusDetail:string,progress:int(0-100),previewImage:string,videoVersions:object,prompt:string,width:int,height:int,settings:object,privacy:string,createdAt:string(iso)}
    Note: Poll every 2-3s until terminal status
    Note: videoVersions["original"] contains the video URL when status=ready. videoPaths is internal.

-   GET /videos
    Query: {page:int(1),limit:int(1-50,10),sortBy:string,sortOrder:string(ASC|DESC),userId:string,username:string,status:string,width:int,height:int,resolution:int,prompt:string,hidePrompt:bool}
    Resp: 200, [{id:string,status:string,prompt:string,...}]
    Note: Pagination in response headers X-Total-Count, X-Current-Page, X-Per-Page, X-Total-Pages

-   PUT /videos/{id}/privacy
    Path: id:string(uuid,req)
    Body: {privacy:string(public|private|licensed,private)}
    Resp: 204 (success), 400 (error)
    Note: Public videos visible on LetzAI feed

-   PUT /videos/{id}/prompt-privacy
    Path: id:string(uuid,req)
    Resp: 204 (success), 400 (error)
    Note: Hides/shows the prompt on public listings

Note: There is no video interruption endpoint and no /video-edits endpoint on the public API.

### Uploads (User Assets)

Two-step pre-signed S3 upload. Use this to turn a local file into a public URL for /image-edits, /videos, /upscale or model training.

-   POST /user-assets
    Body: {extension:string(jpg|jpeg|png|gif|webp|mp4|webm|mov|avi|mp3|wav|ogg|m4a|aac|flac,req),numberOfImages:int(1),originalFilename:string,mimeType:string,fileSize:int,caption:string,metadata:object}
    Resp: 201, {id:string,uploadUrl:string,imageUrl:string,...}
    Note: Then PUT the raw file bytes to uploadUrl with the matching Content-Type. Send NO Authorization header - the URL is pre-signed.
    Note: After the PUT succeeds the file is public at imageUrl
    Note: numberOfImages > 1 returns several upload URLs in one call

-   GET /user-assets
    Query: {page:int(1),limit:int(max 100)}
    Note: Newest first

-   GET /user-assets/{id}
-   PATCH /user-assets/{id}  Body: {caption:string,metadata:object}
-   DELETE /user-assets/{id}

-   POST/GET /user-images, GET/DELETE /user-images/{id}
    Note: Legacy image+video-only variant of the same flow. Prefer /user-assets.

## Retrieving Images, Videos and Image Edits

Important information to retrieve media and their status

### Images and Upscales

-   Long Poll on GET /images/{id} or GET /upscale/{id} to retrieve status
-   Poll: 3s interval for /images/{id}, /upscale/{id}, /image-edits/{id} until a terminal status
-   Field: imageVersions:{string:string}
-   Description: Maps resolution labels to image URLs
-   Usage:
    -   "original": Full resolution image URL
    -   "1920x1920": 1920x1920 resized image URL
    -   "640x640": 640x640 resized image URL
-   Action: After status=ready, fetch URL from imageVersions["original"] for full file, or ["1920x1920"]/["640x640"] for specific sizes
-   Note: For image-edits, both originalImageCompletion and generatedImageCompletion have imageVersions; the result is generatedImageCompletion.imageVersions["original"]

### Videos

-   Long Poll on GET /videos/{id} to retrieve status
-   Poll: 2-3s interval until a terminal status
-   Field: videoVersions:{string:string}
-   Description: Maps video version labels to video URLs (e.g. "original" → video URL)
-   Action: After status=ready, fetch URL from videoVersions["original"] for video file
-   Note: previewImage field contains a preview during generation
-   Note: videoPaths is an internal field and is not part of the public contract

## Supabase Edge Functions Example

Setup:

-   CLI: npm install -g @supabase/supabase-cli
-   Init: supabase init
-   New: supabase functions new letzai-wrapper
-   Env: LETZAI_API_KEY (Dashboard)

Function (index.ts):
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
serve(async req => {
const { endpoint, method, body } = await req.json();
const res = await fetch(https://api.letz.ai${endpoint}, {
method: method || "GET",
headers: {"Content-Type":"application/json","Authorization":Bearer ${Deno.env.get("LETZAI_API_KEY")}},
body: method != "GET" ? JSON.stringify(body) : undefined
});
return new Response(JSON.stringify(await res.json()), {status:res.status,headers:{"Content-Type":"application/json"}});
});

## Examples

JS (Images):
fetch("https://<project-ref>.supabase.co/functions/v1/letzai-wrapper", {
method:"POST", headers:{"Content-Type":"application/json","Authorization":"Bearer <SUPABASE_ANON_KEY>"},
body:JSON.stringify({endpoint:"/images",method:"POST",body:{prompt:"A city"}})
}).then(res => res.json()).then(data => {
const id = data.id;
const poll = setInterval(() => {
fetch("https://<project-ref>.supabase.co/functions/v1/letzai-wrapper", {
method:"POST", headers:{"Content-Type":"application/json","Authorization":"Bearer <SUPABASE_ANON_KEY>"},
body:JSON.stringify({endpoint:`/images/${id}`})
}).then(res => res.json()).then(status => {
if (status.status === "ready" || status.status === "failed") clearInterval(poll);
});
}, 3000);
// Interrupt: fetch("...", {body:JSON.stringify({endpoint:`/images/${id}/interruption`,method:"PUT"})})
// Privacy: fetch("...", {body:JSON.stringify({endpoint:`/images/${id}/privacy`,method:"PUT",body:{privacy:"public"}})})
});

JS (Videos):
fetch("https://<project-ref>.supabase.co/functions/v1/letzai-wrapper", {
method:"POST", headers:{"Content-Type":"application/json","Authorization":"Bearer <SUPABASE_ANON_KEY>"},
body:JSON.stringify({endpoint:"/videos",method:"POST",body:{prompt:"waves on beach",imageUrl:"https://example.com/image.jpg",width:1024,height:1024,settings:{mode:"video-kling26",duration:5}}})
}).then(res => res.json()).then(data => {
const id = data.id;
const poll = setInterval(() => {
fetch("https://<project-ref>.supabase.co/functions/v1/letzai-wrapper", {
method:"POST", headers:{"Content-Type":"application/json","Authorization":"Bearer <SUPABASE_ANON_KEY>"},
body:JSON.stringify({endpoint:`/videos/${id}`})
}).then(res => res.json()).then(status => {
if (status.status === "ready" || status.status === "failed") {
clearInterval(poll);
if (status.videoVersions) console.log("Video URL:", status.videoVersions["original"]);
}
});
}, 3000);
});

Python:
import requests, time
def call(endpoint, method="GET", body={}):
res = requests.post("https://<project-ref>.supabase.co/functions/v1/letzai-wrapper",
json={"endpoint":endpoint,"method":method,"body":body},
headers={"Content-Type":"application/json","Authorization":"Bearer <SUPABASE_ANON_KEY>"})
return res.json()
resp = call("/images", "POST", {"prompt":"A city"})
id = resp["id"]
while True:
status = call(f"/images/{id}")
if status["status"] in ["ready", "failed"]: break
time.sleep(3)

Interrupt: call(f"/images/{id}/interruption", "PUT")
Privacy: call(f"/images/{id}/privacy", "PUT", {"privacy":"public"})

# Image Edits Example (Context Editing):

edit_resp = call("/image-edits", "POST", {
"mode": "context",
"prompt": "change the background to a beautiful sunset",
"imageUrl": "https://example.com/image.jpg",
"baseModel": "gemini-3-pro-image",
"settings": {
"aspect_ratio": "16:9",
"model": "gemini-3-pro-image",
"resolution": "2k"
}
})
edit_id = edit_resp["id"]
while True:
edit_status = call(f"/image-edits/{edit_id}")
if edit_status["status"] in ["ready", "failed"]: break
time.sleep(3)

# Access edited image URL from edit_status["generatedImageCompletion"]["imageVersions"]["original"]

# Video Example:

video_resp = call("/videos", "POST", {
"prompt": "waves crashing on beach",
"imageUrl": "https://example.com/beach.jpg",
"width": 1024,
"height": 1024,
"settings": {
"mode": "video-kling26",
"duration": 5,
"highQuality": True
}
})
video_id = video_resp["id"]
while True:
video_status = call(f"/videos/{video_id}")
if video_status["status"] in ["ready", "failed"]: break
time.sleep(3)

# Access video URL from video_status["videoVersions"]["original"]

# Upscale Example:

upscale_resp = call("/upscale", "POST", {
"imageUrl": "https://example.com/image.jpg",
"mode": "nano-banana-pro",
"size": 8,
"strength": 2
})
upscale_id = upscale_resp["id"]
while True:
upscale_status = call(f"/upscale/{upscale_id}")
if upscale_status["status"] in ["ready", "failed"]: break
time.sleep(3)

# Access upscaled image URL from upscale_status["imageVersions"]["original"]

## Security Notes for Supabase

Use LETZAI_API_KEY in Edge Function env only, never in public code
