# Vercel AI SDK
Source: https://docs.platform.decart.ai/ai-frameworks/ai-sdk
Generate images and videos with Decart using the Vercel AI SDK.
The Decart provider for the [Vercel AI SDK](https://ai-sdk.dev/docs) adds image and video generation to any AI SDK project.
## Prerequisites
* A [Decart API key](/getting-started/authentication)
* Node.js 18+
* The `ai` package (`npm i ai`)
## Installation
```bash npm theme={null}
npm install @decartai/ai-sdk-provider
```
```bash pnpm theme={null}
pnpm add @decartai/ai-sdk-provider
```
```bash yarn theme={null}
yarn add @decartai/ai-sdk-provider
```
## Setup
Import the default provider instance:
```typescript theme={null}
import { decart } from '@decartai/ai-sdk-provider';
```
The provider reads your API key from the `DECART_API_KEY` environment variable by default.
Set your Decart API key as an environment variable:
```bash theme={null}
export DECART_API_KEY="your-api-key"
```
Get your API key from [platform.decart.ai](https://platform.decart.ai). See [authentication](/getting-started/authentication) for details.
For custom configuration, use `createDecart()`:
```typescript theme={null}
import { createDecart } from '@decartai/ai-sdk-provider';
const decart = createDecart({
apiKey: 'your-api-key',
baseURL: 'https://api.decart.ai',
});
```
API key for authentication. Defaults to the `DECART_API_KEY` environment variable.
Custom URL prefix for API calls. Defaults to `https://api.decart.ai`.
Custom headers to include in requests.
Custom fetch implementation for intercepting requests or testing.
## Image generation
Generate images with [`generateImage()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/generate-image):
```typescript theme={null}
import { decart } from '@decartai/ai-sdk-provider';
import { generateImage } from 'ai';
import fs from 'fs';
const { image } = await generateImage({
model: decart.image('lucy-pro-t2i'),
prompt: 'A golden retriever sitting in a field of wildflowers at sunset',
});
const filename = `image-${Date.now()}.png`;
fs.writeFileSync(filename, image.uint8Array);
console.log(`Image saved to ${filename}`);
```
### Image models
| Model | Description |
| - | - |
| `lucy-pro-t2i` | High-quality text-to-image generation |
### Image settings
Aspect ratio of the generated image. Supported values: `16:9` (landscape) and `9:16` (portrait).
Seed value for reproducible results.
Unsupported aspect ratios generate a warning and fall back to the default behavior.
```typescript theme={null}
const { image } = await generateImage({
model: decart.image('lucy-pro-t2i'),
prompt: 'A minimalist Japanese garden with cherry blossoms',
aspectRatio: '16:9',
seed: 42,
});
```
## Video generation
Generate videos with [`experimental_generateVideo()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/generate-video):
`experimental_generateVideo` is an experimental AI SDK API. The function signature may change in future releases.
### Text-to-video
```typescript theme={null}
import { decart } from '@decartai/ai-sdk-provider';
import { experimental_generateVideo as generateVideo } from 'ai';
import fs from 'fs';
const { videos } = await generateVideo({
model: decart.video('lucy-pro-t2v'),
prompt: 'A horse galloping through a sunlit meadow',
});
fs.writeFileSync('video.mp4', videos[0].uint8Array);
```
### Video models
| Model | Description |
| - | - |
| `lucy-pro-t2v` | Text-to-video generation |
### Video settings
Aspect ratio of the generated video. Supported values: `16:9` (landscape) and `9:16` (portrait).
Seed value for reproducible results.
Video resolution. Supported values: `1280x720` (720p) and `854x480` (480p).
### Provider options
Pass Decart-specific options via `providerOptions.decart`:
Override orientation directly instead of deriving it from `aspectRatio`.
## Next steps
Explore all available Decart models.
Use Decart's native SDK for full API access.
View source code and contribute.
# LangChain.js
Source: https://docs.platform.decart.ai/ai-frameworks/langchain
Give your LLM agents the ability to generate and edit images with Decart.
The `@decartai/langchain` package provides a [LangChain Tool](https://js.langchain.com/docs/concepts/tools) that gives any LLM agent the ability to generate and edit images using Decart.
## Prerequisites
* A [Decart API key](/getting-started/authentication)
* Node.js 18+
* An LLM provider (Anthropic, OpenAI, etc.) for agent usage
## Installation
Install the package along with the required `@langchain/core` peer dependency:
```bash npm theme={null}
npm install @decartai/langchain @langchain/core
```
```bash pnpm theme={null}
pnpm add @decartai/langchain @langchain/core
```
```bash yarn theme={null}
yarn add @decartai/langchain @langchain/core
```
## Setup
Set your Decart API key as an environment variable:
```bash theme={null}
export DECART_API_KEY="your-api-key"
```
Get your API key from [platform.decart.ai](https://platform.decart.ai). See [authentication](/getting-started/authentication) for details.
Or pass your API key directly to the tool:
```typescript theme={null}
import { DecartImageTool } from '@decartai/langchain';
const tool = new DecartImageTool({ apiKey: 'your-api-key' });
```
## Quick start
### Text-to-image
Generate an image from a text prompt:
```typescript theme={null}
import { DecartImageTool } from '@decartai/langchain';
const tool = new DecartImageTool();
const image = await tool.invoke({
prompt: 'A serene mountain landscape at sunset',
resolution: '720p',
});
// Returns: data:image/png;base64,...
```
### Image editing
Edit an existing image by providing an `imageUrl`:
```typescript theme={null}
const editedImage = await tool.invoke({
prompt: 'Change the sky to aurora borealis',
imageUrl: 'https://example.com/original.jpg',
});
```
The tool automatically selects the right model: `lucy-pro-t2i` for text-to-image (no `imageUrl`), and `lucy-image-2` for image-to-image editing (with `imageUrl`).
## Using with LLM agents
Bind the tool to an LLM to let it generate images on its own:
```typescript theme={null}
import { ChatAnthropic } from '@langchain/anthropic';
import { HumanMessage } from '@langchain/core/messages';
import { DecartImageTool } from '@decartai/langchain';
const llm = new ChatAnthropic({ model: 'claude-sonnet-4-20250514' });
const tool = new DecartImageTool();
const llmWithTools = llm.bindTools([tool]);
const response = await llmWithTools.invoke([
new HumanMessage('Generate an image of a futuristic city at night'),
]);
if (response.tool_calls?.length > 0) {
const result = await tool.invoke(response.tool_calls[0].args);
console.log('Generated image:', result);
}
```
This works with any LangChain-compatible LLM that supports tool calling — including OpenAI, Google, and Mistral models.
## Parameters
Text description for image generation, or an edit instruction for image-to-image.
Output resolution.
Output orientation. Only applies to text-to-image generation.
Seed value for reproducible results.
Source image URL for image-to-image editing. When provided, the tool uses `lucy-image-2` instead of `lucy-pro-t2i`.
Automatically enhance the prompt for better results.
## Model selection
The tool selects the model automatically based on your input:
| Input | Model | Mode |
| - | - | - |
| `prompt` only | `lucy-pro-t2i` | Text-to-image generation |
| `prompt` + `imageUrl` | `lucy-image-2` | Image-to-image editing |
## Constructor options
Decart API key. Defaults to the `DECART_API_KEY` environment variable.
Custom API base URL.
## Next steps
Explore all available Decart models.
Use Decart's native SDK for full API access.
View source code and contribute.
# Frameworks
Source: https://docs.platform.decart.ai/ai-frameworks/overview
Use Decart image and video generation through popular AI frameworks like Vercel AI SDK, TanStack AI, and LangChain.
Use Decart's image and video generation models through the AI framework you already use, instead of integrating with the API directly.
## Choosing a framework
* **Already using Vercel AI SDK?** Use the [AI SDK provider](/ai-frameworks/ai-sdk). It has the most models and the richest feature set.
* **Building with TanStack?** Use the [TanStack AI adapter](/ai-frameworks/tanstack-ai).
* **Using LangChain.js?** Use the [LangChain.js tool](/ai-frameworks/langchain). It also supports image editing and automatic tool calling.
* **Want the most control?** Use [Decart's native JavaScript SDK](/sdks/javascript) directly.
## Prerequisites
All three integrations require a Decart API key. Get yours from [platform.decart.ai](https://platform.decart.ai) — see [authentication](/getting-started/authentication) for setup instructions.
Image and video generation with 5 models.
Image and video generation for TanStack and React apps.
Image generation and editing as a tool for LLM agents.
# TanStack AI
Source: https://docs.platform.decart.ai/ai-frameworks/tanstack-ai
Generate images and videos with Decart using TanStack AI.
The Decart adapter for [TanStack AI](https://tanstack.com/ai) adds image and video generation to any TanStack project.
Decart is listed as an [official community adapter](https://tanstack.com/ai/latest/docs/community-adapters/decart) on the TanStack AI docs.
## Prerequisites
* A [Decart API key](/getting-started/authentication)
* Node.js 18+
## Installation
Install both the Decart adapter and TanStack AI:
```bash npm theme={null}
npm install @decartai/tanstack-ai-adapter @tanstack/ai
```
```bash pnpm theme={null}
pnpm add @decartai/tanstack-ai-adapter @tanstack/ai
```
```bash yarn theme={null}
yarn add @decartai/tanstack-ai-adapter @tanstack/ai
```
## Setup
Set your Decart API key as an environment variable:
```bash theme={null}
export DECART_API_KEY="your-api-key"
```
Get your API key from [platform.decart.ai](https://platform.decart.ai). See [authentication](/getting-started/authentication) for details.
Or pass your API key directly when creating adapters:
```typescript theme={null}
import { createDecartImage } from '@decartai/tanstack-ai-adapter';
const adapter = createDecartImage('lucy-pro-t2i', 'your-api-key');
```
## Image generation
Generate images with `generateImage()` and the `decartImage()` adapter:
```typescript theme={null}
import { generateImage } from '@tanstack/ai';
import { decartImage } from '@decartai/tanstack-ai-adapter';
const result = await generateImage({
adapter: decartImage('lucy-pro-t2i'),
prompt: 'A serene mountain landscape at sunset, cinematic lighting',
});
console.log(result.images[0].b64Json); // Base64-encoded image data
```
### With explicit API key
Use `createDecartImage()` to pass your API key directly:
```typescript theme={null}
import { generateImage } from '@tanstack/ai';
import { createDecartImage } from '@decartai/tanstack-ai-adapter';
const result = await generateImage({
adapter: createDecartImage('lucy-pro-t2i', 'your-api-key'),
prompt: 'A serene mountain landscape at sunset, cinematic lighting',
});
```
## Video generation
Video generation uses an async job and polling pattern — you submit a job, then poll for the result.
Unlike `generateImage()`, video generation is asynchronous. You submit a generation job with `generateVideo()`, then poll its status with `getVideoJobStatus()` until it completes.
```typescript theme={null}
import { setTimeout } from 'node:timers/promises';
import { generateVideo, getVideoJobStatus } from '@tanstack/ai';
import { decartVideo } from '@decartai/tanstack-ai-adapter';
const { jobId } = await generateVideo({
adapter: decartVideo('lucy-pro-t2v'),
prompt: 'A cat walking through a miniature lego city',
});
let videoUrl: string | undefined;
while (true) {
const status = await getVideoJobStatus({
adapter: decartVideo('lucy-pro-t2v'),
jobId,
});
if (status.status === 'failed') {
throw new Error('Video generation failed');
}
if (status.status === 'completed') {
videoUrl = status.url;
break;
}
await setTimeout(2000);
}
console.log('Video ready:', videoUrl);
```
## Models
| Model | Type | Description |
| - | - | - |
| `lucy-pro-t2i` | Text-to-image | High-quality image generation |
| `lucy-pro-t2v` | Text-to-video | Video generation from text prompts |
## Options
Seed value for reproducible results.
Output resolution. Defaults to `720p`.
## Next steps
Explore all available Decart models.
Use Decart's native SDK for full API access.
View source code and contribute.
# Create Client Token
Source: https://docs.platform.decart.ai/api-reference/create-client-token
/openapi.json post /v1/client/tokens
Create an ephemeral API token for client sessions.
# Delete File
Source: https://docs.platform.decart.ai/api-reference/delete-file
/openapi.json delete /v1/files/{file_id}
Delete an uploaded file early, before its TTL expires.
# Get File By MD5
Source: https://docs.platform.decart.ai/api-reference/get-file-by-md5
/openapi.json get /v1/files/by-md5/{md5}
Look up an uploaded file by the MD5 of its bytes, without knowing its `file_*` id. Returns the newest non-expired match when the same file was uploaded more than once. Compute the hash over the exact bytes you uploaded (for example `md5sum image.png`).
# Get File Metadata
Source: https://docs.platform.decart.ai/api-reference/get-file-metadata
/openapi.json get /v1/files/{file_id}
Get metadata for an uploaded file.
# Get Job
Source: https://docs.platform.decart.ai/api-reference/get-job
/openapi.json get /v1/jobs/{job_id}
Get the status and metadata of a job by its ID. Poll this endpoint to check if your job has completed.
# Get Job Content
Source: https://docs.platform.decart.ai/api-reference/get-job-content
/openapi.json get /v1/jobs/{job_id}/content
Download the result of a completed job.
Returns:
Binary content (image/video) or error if not ready/not found
# Get Realtime Quota
Source: https://docs.platform.decart.ai/api-reference/get-realtime-quota
/openapi.json get /v1/realtime/quota
Current concurrent realtime session quota state for the authenticated account.
For org-scoped keys the count is org-wide (shared across the org's keys/users).
`active` is a live counter, not a periodic aggregate: a session is counted the moment its connection is authenticated and released the moment it closes, so a read reflects a connect or disconnect within about a second. If one of our gateway instances crashes mid-session, its sessions can stay counted for up to 45 seconds before they expire.
Use this endpoint for coarse pre-flight checks and dashboards: poll at most once per second and stay under 10 requests per second per API key. `remaining` is advisory, since other sessions can start between your read and your connect, so always handle the connect-time rejection (WebSocket close code 1013, "Concurrent session limit reached.").
# List Files
Source: https://docs.platform.decart.ai/api-reference/list-files
/openapi.json get /v1/files
List your uploaded files, newest first. Expired files are omitted, as they 404 on `GET /v1/files/{file_id}`.
Pages continue from the last file returned rather than from an offset, so an upload or delete between requests never repeats or skips a file. Pass the previous page's `next_cursor` as `cursor`; the last page has `next_cursor: null`.
# Lucy 2.1
Source: https://docs.platform.decart.ai/api-reference/lucy-21
/openapi.json post /v1/jobs/lucy-2.1
Edit video using Lucy 2.1 model with optional prompt and reference image.
# Lucy 2.5
Source: https://docs.platform.decart.ai/api-reference/lucy-25
/openapi.json post /v1/jobs/lucy-2.5
Edit video using Lucy 2.5 model with optional prompt and reference image. Supports self-anchoring (on by default).
# Lucy Image 2
Source: https://docs.platform.decart.ai/api-reference/lucy-image-2
/openapi.json post /v1/generate/lucy-image-2
Edit image from image using Lucy Image 2 model.
# Lucy Image Latest
Source: https://docs.platform.decart.ai/api-reference/lucy-image-latest
/openapi.json post /v1/generate/lucy-image-latest
Edit image from image using the latest Lucy Image model.
# Lucy Latest
Source: https://docs.platform.decart.ai/api-reference/lucy-latest
/openapi.json post /v1/jobs/lucy-latest
Edit video using the latest Lucy video model with optional prompt and reference image.
# Lucy Restyle 2
Source: https://docs.platform.decart.ai/api-reference/lucy-restyle-2
/openapi.json post /v1/jobs/lucy-restyle-2
Restyle video using Lucy Restyle 2 model.
# Lucy Restyle Latest
Source: https://docs.platform.decart.ai/api-reference/lucy-restyle-latest
/openapi.json post /v1/jobs/lucy-restyle-latest
Restyle video using the latest Lucy Restyle model.
# Lucy VTON 3.5
Source: https://docs.platform.decart.ai/api-reference/lucy-vton-35
/openapi.json post /v1/jobs/lucy-vton-3.5
Edit video with virtual try-on using the Lucy VTON 3.5 model.
# Lucy VTON 3.6
Source: https://docs.platform.decart.ai/api-reference/lucy-vton-36
/openapi.json post /v1/jobs/lucy-vton-3.6
Edit video with virtual try-on using the Lucy VTON 3.6 model.
# Lucy VTON Latest
Source: https://docs.platform.decart.ai/api-reference/lucy-vton-latest
/openapi.json post /v1/jobs/lucy-vton-latest
Edit video with virtual try-on using the latest Lucy VTON model.
# Resolve Model Aliases
Source: https://docs.platform.decart.ai/api-reference/resolve-model-aliases
/openapi.json post /v1/models/resolve
Resolve one or more public model names/aliases to canonical model ids.
# Upload File
Source: https://docs.platform.decart.ai/api-reference/upload-file
/openapi.json post /v1/files
Upload a file once and get back a `file_*` reference id you can reuse across generations.
The same reference can be passed to realtime `set_image` as `image_ref`, avoiding a re-upload of the underlying bytes on every call.
# Changelog
Source: https://docs.platform.decart.ai/changelog
Product updates and announcements for the Decart API Platform
## Fresh Client Tokens on Every Realtime Dial, Lucy VTON 3.6 (Now Latest, with Fast Mode) & One API Hostname
* **Fresh client tokens on every realtime dial** - Client tokens expire 60 seconds after minting by default, so a token minted on page load was often refused by the time the user pressed start, and a reconnect with the same token failed with a generic connection error. `createDecartClient` in the [JavaScript](/sdks/javascript-realtime#client-side-authentication) SDK (v0.2.6) now accepts `apiKeyProvider`, a function the SDK calls right before every connect, reconnect and subscribe, so each dial carries a token minted for it. An already-expired token is rejected with `TOKEN_EXPIRED` before any socket is opened. The Studio playgrounds use the same pattern. See [Token lifetime](/getting-started/client-tokens#token-lifetime)
* **Lucy VTON 3.6** - New virtual try-on model (`lucy-vton-3.6`) available on realtime and batch surfaces, at 720p. It is **opt-in by name**: `lucy-vton-latest` continues to resolve to `lucy-vton-3.5`. Unlike 3.5, `lucy-vton-3.6` does not offer [fast mode](/models/realtime/lucy-2.5#fast-mode) — `speed: "fast"` is ignored and the session is billed at the standard rate. Available across [JavaScript](/sdks/javascript) (v0.2.7), [Python](/sdks/python), [Android](/sdks/android), and [Swift](/sdks/swift) SDKs, and selectable from the Virtual Try-On playground's version dropdown. See the [Prompting Guide](/models/realtime/vton-3.5-prompting) and [Pricing](/getting-started/pricing#realtime-models)
* **Lucy VTON 3.6 is now the latest, with fast mode** - `lucy-vton-latest` now resolves to `lucy-vton-3.6`, and 3.6 now supports [fast mode](/models/realtime/virtual-try-on#fast-mode) (`speed: "fast"`, billed at 2x the standard realtime rate, \$0.04/sec) on the realtime surface. `lucy-vton-3.5` remains available by name, with fast mode. The Virtual Try-On playground now defaults to 3.6 on both surfaces, and its Fast mode toggle is available on either version. The SDKs list `fast` among `lucy-vton-3.6`'s supported speeds from their next release. See [Pricing](/getting-started/pricing#realtime-models)
* **One API hostname** - `api.decart.ai` now serves realtime signaling (WebSocket) as well as the REST API, and is the only hostname the docs and SDK defaults use. `api3.decart.ai` keeps working indefinitely for existing integrations; new integrations and firewall allowlists only need `api.decart.ai`. See [Network Requirements](/integrations/network-requirements#domains-to-allow-production)
## Realtime Fast Mode, Lucy VTON 2 and 3 Retirement, Signed Client Tokens & Network Requirements Guide
* **Realtime fast mode** - Pass `speed: "fast"` when connecting to `lucy-2.5` / `lucy-latest` or `lucy-vton-3.5` / `lucy-vton-latest` for lower latency and higher throughput; output quality is unchanged. Billed at 2x the standard realtime rate (\$0.04/sec). Other models ignore the option; omit it for standard mode. Available across [JavaScript](/sdks/javascript-realtime#fast-mode) (v0.2.2), [Python](/sdks/python-realtime#fast-mode) (v0.2.1), [Android](/sdks/android-realtime#fast-mode) (0.8.1), and [Swift](/sdks/swift-realtime#fast-mode) (v0.7.1) SDKs. The Studio playground gains a matching **Fast mode** switch. See [Fast mode](/models/realtime/lucy-2.5#fast-mode) and [Pricing](/getting-started/pricing#realtime-models)
* **Lucy VTON 2 and 3 retirement** - `lucy-vton-2` and `lucy-vton-3` (and the `lucy-2.1-vton-2` alias) are retired and no longer served. Use `lucy-vton-3.5`, or `lucy-vton-latest` which resolves to it. The ids are removed from the [JavaScript SDK](/sdks/javascript) as of v0.2.0
* **Signed client tokens** - Client tokens from `POST /v1/client/tokens` are now signed, and the gateway verifies them offline against the platform's public JWKS. Realtime connections, file uploads, and batch and queue calls no longer wait on a key lookup. Pass the returned `apiKey` to the SDK as before; no client changes needed. See [Client Tokens](/getting-started/client-tokens)
* **Network requirements guide** - New reference for IT, networking, and security teams deploying the realtime SDK, covering supported OS and hardware, per-session bandwidth, connection-quality thresholds, and the domains and ports that must be allowed through corporate firewalls. See [Network Requirements](/integrations/network-requirements)
## Preset Studio, Automatic Glass-to-Glass & Lucy VTON 3.5
* **Preset Studio** - New hub at [platform.decart.ai/studio](https://platform.decart.ai/studio) to save, organize, share, and version reusable prompts and reference images. Apply presets across supported model playgrounds, share within an organization, and restore earlier versions from version history. See the [Preset Studio guide](/getting-started/preset-studio)
* **Automatic glass-to-glass in JavaScript realtime** - The [JavaScript](/sdks/javascript-realtime#glass-to-glass-latency) SDK (v0.1.21) now measures glass-to-glass latency automatically via LiveKit frame metadata — read `glassToGlass.ttffMs` / `.medianMs` off the `stats` event. `debugQuality: true` is a deprecated no-op
* **Lucy VTON 3.5** - New latest-generation virtual try-on model (`lucy-vton-3.5`) available on realtime and batch surfaces, at 720p. `lucy-vton-latest` now resolves to `lucy-vton-3.5`. Available across [JavaScript](/sdks/javascript) (v0.1.19), [Python](/sdks/python) (v0.1.5), [Android](/sdks/android) (0.7.10), and [Swift](/sdks/swift) (v0.6.10) SDKs. See the [VTON 3.5 Prompting Guide](/models/realtime/vton-3.5-prompting)
## Lucy 2.5
* **Lucy 2.5** - New latest-generation video editing model (`lucy-2.5`) available on realtime and batch surfaces. `lucy-latest` now resolves to `lucy-2.5`. The previous `lucy-2.1` remains live for pinned integrations. Supports text prompts, reference images, or both. See the [Lucy 2.5 guide](/models/realtime/lucy-2.5)
## Oasis 3 Preview, Realtime Quality & Python LiveKit
* **Oasis 3 Preview** - New realtime, promptable world model (`oasis-3-preview`): set a scene with a text prompt, stream driving actions (throttle + steering), and receive generated camera frames in realtime — a learned driving simulator you can run reinforcement learning in. Available via the dedicated [`decart-oasis`](https://pypi.org/project/decart-oasis/) Python gRPC SDK. See the [Oasis 3 Preview guide](/models/realtime/oasis-3) and the [RL training notebook](https://colab.research.google.com/github/DecartAI/decart-robotics/blob/main/notebook/train_oasis3_ppo_colab.ipynb), or try it live at [oasis3-preview.decart.ai](https://oasis3-preview.decart.ai)
* **Lucy VTON 3** - New latest-generation virtual try-on model (`lucy-vton-3`) available on realtime and batch surfaces. `lucy-vton-latest` now resolves to `lucy-vton-3`. The previous `lucy-vton-2` remains live for pinned integrations. Available across [JavaScript](/sdks/javascript) (v0.1.5), [Python](/sdks/python) (v0.0.40), [Android](/sdks/android) (0.7.4), and [Swift](/sdks/swift) (v0.6.3) SDKs
* **Lucy 2.1 VTON retirement** - `lucy-2.1-vton` and its `lucy-vton` alias are no longer served. Use `lucy-vton-3`, `lucy-vton-latest`, or pin to `lucy-vton-2` if you need the previous generation
* **Connection quality, preflight & glass-to-glass latency** - Realtime SDKs now report network health on a shared `good | fair | poor | critical` scale. Run a preflight before connecting (`checkConnectivity()`) to gate the integration, react to in-session quality (`onConnectionQuality`) with the limiting factor (bandwidth, latency, loss, stall, cpu), or opt in to glass-to-glass latency (`debugQuality: true`) to measure real camera→display latency end-to-end. Available in [JavaScript](/sdks/javascript-realtime#connection-quality) (v0.1.6), [Android](/sdks/android-realtime#connection-quality) (0.7.5), and [Swift](/sdks/swift-realtime#connection-quality) (v0.6.4)
* **Python realtime SDK moved to LiveKit** - The [Python](/sdks/python-realtime) realtime SDK (v0.1.0) completes the LiveKit migration after JavaScript, Android, and Swift. Input is now a caller-supplied `livekit.rtc.LocalVideoTrack`, `on_remote_stream` receives a `livekit.rtc.RemoteVideoTrack`, and a new `preferred_video_codec` option picks between `h264` (default) and `vp9`. Install with `pip install decart[realtime]`
## 1080p Realtime & Mobile Mirroring
* **1080p realtime streams** - New `resolution` option on `realtime.connect()` lets supported models deliver 1080p output. Available across [JavaScript](/sdks/javascript-realtime) (v0.0.68), [Python](/sdks/python-realtime) (v0.0.39), [Android](/sdks/android-realtime) (0.6.1), and [Swift](/sdks/swift-realtime) (v0.6.1) SDKs. Defaults to 720p when omitted
* **Front-camera mirroring on iOS & Android** - New `mirror` option pre-flips selfie input so server-baked pixels (watermarks, overlays) render correctly without re-flipping on the receiving end. Now available on [Android](/sdks/android-realtime#front-camera-mirroring) (0.6.0) and [Swift](/sdks/swift-realtime#front-camera-mirroring) (v0.6.0), in addition to JavaScript
* **Lucy VTON 2** - New next-generation virtual try-on model (`lucy-vton-2`) available on realtime and batch surfaces. `lucy-vton-latest` now resolves to `lucy-vton-2`
* **Origin-scoped client tokens** - New `allowedOrigins` option on `client.tokens.create()` pins a client token to a list of canonical web origins; mismatched browser-issued `Origin` headers are rejected at the realtime gateway. Available in the [JavaScript](/sdks/javascript) SDK (v0.0.63) and the [Python](/sdks/python) SDK (v0.0.37). See [Client Tokens → Origin scoping](/getting-started/client-tokens#origin-scoping)
* **Model retirements** - LipSync Live, Live Avatar, Lucy Motion, Lucy v1 Realtime, and Lucy Restyle v1 Realtime are no longer served by the API. Their docs pages now redirect to the relevant category overviews and the SDKs no longer surface their model IDs. Migrate to `lucy-2.1` (realtime + batch editing) or `lucy-restyle-2` (realtime + batch restyling)
* **Custom models in the Python SDK** - The Python SDK now accepts an inline `ModelDefinition` for preview, experimental, or private models that aren't in the built-in registry — matching the JavaScript SDK's `CustomModelDefinition`
* **Realtime SDKs now LiveKit-backed** - The realtime media transport in the [JavaScript](/sdks/javascript-realtime) (v0.1.0) and [Android](/sdks/android-realtime) (0.7.0) SDKs moved to a LiveKit-managed WebRTC session for better reliability and to power LiveKit-based session subscribe; [Swift](/sdks/swift-realtime) followed in v0.6.2 in early June. The `connect()` interface is unchanged, but Android's `setPrompt` is now a `suspend` function — callers must invoke it from a coroutine and handle thrown ack failures
* **Files API for realtime reference images** - The [JavaScript SDK](/sdks/javascript-realtime) (v0.1.1) lets you upload a reference image once via `client.files.upload(...)` and pass the resulting `file_…` id by reference to `connect()` / `set()` / `setImage()`, instead of re-encoding the asset on every call
* **Enterprise SSO for organizations** - Organizations can now sign in via their corporate identity provider (Okta, Entra, Google Workspace, or any OIDC IdP). Org owners and admins self-serve provider registration and DNS TXT domain verification from organization settings; new SSO sign-ins are JIT-provisioned into the organization as members
## Lucy 2.1, Virtual Try-On & Team Accounts
* **Lucy 2.1** - New flagship realtime and batch video editing model (`lucy-2.1`). Available across [JavaScript](/sdks/javascript), [Python](/sdks/python), [Android](/sdks/android), and [Swift](/sdks/swift) SDKs
* **Virtual Try-On** - Launched [`lucy-2.1-vton`](/models/realtime/virtual-try-on) for realtime and batch virtual try-on, with a new e-commerce integration guide
* **Canonical SDK model names** - Unified model naming across all SDKs (e.g. `lucy-2.1`, `lucy-image-2`, `lucy-restyle-2`, `lucy-clip`, `live-avatar`) with deprecation warnings for legacy names
* **`-latest` aliases** - Use `lucy-latest`, `lucy-vton-latest`, `lucy-restyle-latest`, `lucy-clip-latest`, `lucy-motion-latest`, or `lucy-image-latest` to automatically target the newest model version without code changes
* **Reference images for image editing** - Added `reference_image` (file or URL) support to `lucy-image-2` for image-to-image editing
* **Team accounts** - Create organizations with shared credits and billing, invite members by email, manage roles and ownership transfer, and scope API keys to an organization
* **Auto top-up credits** - Save a Stripe payment method and automatically refill credits when your balance drops below a threshold (now also available for organizations)
* **Billing portal & receipts** - Manage payment methods and invoices through a hosted Stripe portal, and download receipts directly from a redesigned transaction history
* **API Terms of Service & DPA** - Published formal [API Terms of Service](/resources/api-terms) and [Data Processing Addendum](/resources/dpa)
* **Realtime Playground** - Added a mobile version selector and improved auto-sent image handling
## Client Tokens v2, Android SDK & Model Simplification
* **Client tokens v2** - [Client tokens](/getting-started/client-tokens) now support configurable TTL, model scoping, and per-session constraints for finer-grained client-side access control
* **Android SDK** - Published the [Decart Android SDK](/sdks/android) with realtime ([`android-realtime`](/sdks/android-realtime)) and batch ([`android-queue`](/sdks/android-queue)) APIs, upload progress callbacks, and Android code snippets across the Lucy 2 docs
* **LipSync Live pricing and guides** - Added full pricing and a complete guide for realtime lip syncing
* **Simpler model names** - Renamed models across docs and the platform with consistent naming, and separated flagship from legacy models in the sidebar
* **Lucy Edit consolidation** - Unified Lucy Edit v1 and v2 into a single docs page with a version selector
* **Mirage → Lucy Restyle Live** - Renamed Mirage to Lucy Restyle Live across docs, the platform, and all SDKs
## Realtime SDK Overhaul & Lucy 2
* **Lucy 2 Realtime** - Launched Lucy 2 (`lucy-2`), a new realtime video-to-video model with character reference image support
* **Session viewing** - Other clients can now watch an active realtime session as read-only viewers using a [subscribe token](/sdks/javascript-realtime#session-viewing-subscribe)
* **Unified `set()` method** - Atomically replace session state (prompt and reference image) in a single call across [JavaScript](/sdks/javascript-realtime#unified-state-update) and [Python](/sdks/python-realtime#unified-state-update) SDKs
* **Auto-reconnect** - Realtime SDKs now automatically reconnect with exponential backoff when connections drop
* **`generationTick` event** - Track session duration and billing with a new periodic event during generation
* **New connection states** - Added `"generating"` and `"reconnecting"` states for better lifecycle visibility
## Lucy 2 RT Preview, Reference Images & Avatar Live
* **Lucy 2 RT (Preview)** - Early access to Lucy 2 realtime at 720p\@20FPS
* **Reference images** - Added reference image support for Lucy Restyle Live, Lucy Restyle, Lucy Pro, and Lucy Edit models in realtime
* **Avatar Live** - New model for audio-driven avatar animation with MP3 support
* **Server proxy** - New [server proxy integration](/sdks/javascript) and Next.js proxy adapter for backend-to-backend SDK usage
* **React Native support** - JavaScript SDK now supports file uploads from React Native
* **Python 3.10+** - Python SDK now requires Python 3.10 or later
## Lucy Restyle & Client Tokens
* **Lucy Restyle** - New video restyling model (`lucy-restyle-2`) for transforming video aesthetics with up to 30-minute input
* **Client tokens** - New [client tokens API](/getting-started/client-tokens) for secure short-lived token creation in client-side apps
* **Unified generate endpoint** - All model generation consolidated into a single API route
* **Image cropping** - Crop uploaded images before processing in the Studio
* **Reduced image pricing** - Lower pricing for image generation models
## Queue API & AWS Marketplace
### New Features
* **Queue-based API** - Introduced queue API pattern for video generation, enabling better handling of long-running jobs
* **AWS Marketplace** - Decart API Platform is now available on [AWS Marketplace](https://aws.amazon.com/marketplace) with integrated billing
* **Showcase page** - Added a new showcase page highlighting community creations and use cases
* **Lucy Fast** - Added Lucy Edit Fast model for quicker video-to-video transformations
### API & SDK Updates
* Updated model routes and code examples for job submission workflow
* Improved error messages when content violates Terms of Service
* Updated video pricing for 720p resolution
### Platform Improvements
* Enhanced billing page with AWS Marketplace support
* Improved credits reserve and deduction logic
* Better video expand functionality in the video player
## Lip Syncing, Motion & Platform Redesign
### New Features
* **Lip Syncing** - Added realtime lip-syncing capabilities to sync audio with video
* **Lucy Motion** - Introduced Lucy Motion model for object animation and movement
* **Lucy Restyle Live v2** - Upgraded to Lucy Restyle Live v2 as the default realtime model with improved quality
* **Video recording** - Added ability to record sessions directly in the Realtime Playground
### Platform Redesign
* Launched redesigned homepage with improved navigation
* New model page in Studio with better organization
* Redesigned sign-in page and authentication flow
* Mobile-optimized Realtime Playground experience
* Renamed "Studio" to "Models" for clarity
### SDK & Documentation
* Released [Python SDK](/sdks/python) documentation
* Added [Swift SDK](/sdks/swift) for iOS development
* Added comprehensive lip-syncing documentation
* Upgraded to Next.js 16 for improved performance
### Developer Experience
* Idle detection with warning dialog in Realtime Playground
* Improved ephemeral API key handling
* Added preset functionality for quick model configuration
* GDPR-compliant analytics with consent management
# Lucy Edit In ComfyUI
Source: https://docs.platform.decart.ai/examples/comfyui
Lucy Edit ComfyUI Cookbook: Text-Guided Video Editing
## Lucy Edit transforms videos with text prompts while preserving motion
Lucy Edit is a video editing model that performs instruction-guided edits on videos using free-text prompts. The model supports clothing changes, character replacements, object insertions, and scene transformations while preserving the original motion and composition perfectly. This cookbook shows you how to set up and use Lucy Edit in ComfyUI for professional video editing results.
## What Lucy Edit does best
**Motion preservation keeps videos smooth.** Lucy Edit maintains the original motion and composition of your videos perfectly, allowing precise edits without disrupting the flow.
**Edit reliability improves over other methods.** Compared to common inference time methods, Lucy Edit produces more robust and consistent edits.
**Identity conservation maintains character features.** When changing clothing or accessories, the model preserves the subject's identity and facial features.
**No masks or finetuning needed.** Pure text instructions drive all edits—you don't need to create masks or finetune models for common editing tasks.
## Installation takes three steps
### Step 1: Clone the repository into ComfyUI
Navigate to your ComfyUI's custom\_nodes folder and clone the Lucy Edit repository:
```bash theme={null}
cd ComfyUI/custom_nodes
git clone https://github.com/DecartAI/lucy-edit-comfyui.git
```
### Step 2: Install Python dependencies
Install the required Python packages:
```bash theme={null}
cd lucy-edit-comfyui
pip install -r requirements.txt
```
### Step 3: Download model weights
**Choose the right weights for your system.** Download either FP16 (smaller, faster) or FP32 (full precision) weights based on your hardware capabilities:
* **FP16 weights (recommended for most users):**
[https://huggingface.co/decart-ai/Lucy-Edit-Dev-ComfyUI/resolve/main/lucy-edit-dev-cui-fp16.safetensors](https://huggingface.co/decart-ai/Lucy-Edit-Dev-ComfyUI/resolve/main/lucy-edit-dev-cui-fp16.safetensors)
* **FP32 weights (for maximum precision):**
[https://huggingface.co/decart-ai/Lucy-Edit-Dev-ComfyUI/resolve/main/lucy-edit-dev-cui.safetensors](https://huggingface.co/decart-ai/Lucy-Edit-Dev-ComfyUI/resolve/main/lucy-edit-dev-cui.safetensors)
Place the downloaded weights in: `ComfyUI/models/diffusion_models/`
## Two ways to run Lucy Edit
### Option 1: Lucy Edit Pro uses the API for faster processing
**API processing offloads computation.** Lucy Edit Pro runs on Decart's servers, requiring no local GPU resources.
1. Load the workflow from `examples/basic-api-lucy-edit.json`
2. Get an API key from [https://platform.decart.ai/](https://platform.decart.ai/)
3. Enter your API key in the workflow node
4. Upload your video and enter your edit prompt
5. Run the workflow to process your edit
### Option 2: Lucy Edit Dev runs entirely on your machine
**Local processing gives you full control.** Lucy Edit Dev runs on your hardware, ideal for sensitive content or offline work.
1. Load the workflow from `examples/basic-lucy-edit-dev.json`
2. Ensure model weights are in the correct folder
3. Upload your video and enter your edit prompt
4. Run the workflow to process locally
**System requirements for local processing:**
* GPU with at least 16GB VRAM (24GB recommended)
* 32GB system RAM minimum
* CUDA-compatible graphics card
## Six types of edits you can make
### 1. Clothing changes work best
**Lucy Edit excels at wardrobe swaps.** The model performs clothing changes with the highest accuracy and consistency.
Write clothing prompts with specific details:
* ✅ "Change the shirt to a silk kimono with wide sleeves and cherry blossom patterns"
* ❌ "Change clothes"
Include material, fit, and style details for best results. Describe textures (silk, leather, denim), patterns (stripes, florals), and fit (loose, tight, flowing).
### 2. Character replacements transform subjects reliably
**Replace humans with creatures or characters.** This edit type works well for transforming people into animals, monsters, or fictional characters.
Effective character replacement prompts:
* ✅ "Replace the person with a Bengal tiger, striped orange fur, muscular build, alert ears"
* ✅ "Replace the woman with an anime character, large expressive eyes, blue hair, school uniform"
* ❌ "Make them a tiger"
Describe physical characteristics, textures, and distinctive features. Avoid pronouns like "him" or "her"—use "person," "man," or "woman" instead.
### 3. Object replacements maintain structure
**Swap objects while preserving size and position.** Object replacement works best when the new object has similar dimensions to the original.
Structure-preserving object swaps:
* ✅ "Replace the apple with a crystal ball, clear glass, internal light refractions, palm-sized"
* ✅ "Replace the coffee cup with a medieval goblet, pewter metal, ornate engravings"
* ❌ "Change the apple"
Keep replacement objects plausible in scale and context. A handheld item should replace another handheld item.
### 4. Color changes need precise descriptions
**Color edits show mixed reliability.** Some color changes are subtle, others dramatic. Detailed descriptions improve consistency.
Color change prompts that work:
* ✅ "Change the jacket color to deep burgundy leather with matte finish"
* ✅ "Change the car paint to metallic silver with mirror-like reflections"
* ❌ "Make it red"
Specify the exact shade, material properties, and finish (matte, glossy, metallic).
### 5. Adding objects works for wearables and props
**Added items often attach to subjects.** This edit type works best for accessories, handheld items, or wearable objects.
Successful object additions:
* ✅ "Add aviator sunglasses on the person's face, gold frames, reflective lenses"
* ✅ "Add a leather backpack on the person's shoulders, brown, worn texture"
* ⚠️ "Add a floating balloon" (may attach to person instead of floating)
Specify exactly where the object should appear and how it connects to the scene.
### 6. Global transformations change entire scenes
**Scene-wide changes may alter subjects.** Transform backgrounds, lighting, or artistic style—but be aware this can affect character identity.
Global transformation examples:
* ✅ "Transform the sunny beach into a snowy winter scene, falling snowflakes, overcast sky"
* ✅ "Transform to 2D cartoon style, bold outlines, flat colors, cel-shaded appearance"
* ⚠️ May change subject appearance along with the scene
Use global transformations when you want comprehensive scene changes. For preserving subjects while changing backgrounds, combine with specific preservation instructions.
## Write prompts that get better results
### Start every prompt with the right trigger word
**Trigger words tell the model your intent.** Beginning with the correct trigger word improves edit accuracy:
* **"Change"** → Clothing or color modifications
* **"Add"** → Adding accessories or objects
* **"Replace"** → Subject or object substitution
* **"Transform"** → Global scene or style changes
### Include 20-30 descriptive words for best results
**Details improve edit precision.** Short prompts like "change shirt to red" produce generic results. Rich descriptions yield professional edits.
Compare these prompts:
* Basic: "Change shirt to kimono"
* Better: "Change the shirt to a silk kimono with deep indigo dye, wide flowing sleeves, subtle crane pattern embroidery, loose fit, soft drape, natural folds"
### Describe materials, textures, and visual properties
**Concrete visual details guide the model.** Include:
* Materials (silk, leather, cotton, metal)
* Textures (fuzzy, smooth, rough, glossy)
* Patterns (stripes, florals, geometric)
* Lighting (soft, harsh, dramatic)
* Scale and positioning
* Colors with modifiers (deep, pale, vibrant)
### Avoid these common prompt mistakes
**Don't mention what to preserve.** The model already preserves motion and pose—mentioning it can confuse the edit.
* ❌ "Change the shirt to red while preserving the pose"
* ✅ "Change the shirt to bright red cotton with ribbed texture"
**Don't use personal pronouns.** Replace "me," "him," "her" with specific descriptors.
* ❌ "Replace me with a robot"
* ✅ "Replace the person with a chrome robot"
**Don't reference specific people.** Avoid mentioning hair color or identifying features.
* ❌ "Change the blonde woman's dress"
* ✅ "Change the dress to emerald green silk"
## Troubleshooting common issues
### Edit not applying strongly enough
**Increase prompt detail.** Add more descriptive words about materials, textures, and visual properties. Consider adjusting the guidance scale in your ComfyUI workflow.
### Motion becoming unstable
**Use 81-frame generations.** Longer clips (81 frames) produce better temporal consistency than shorter ones. Ensure your input video is properly formatted and stable.
### Character identity changing unexpectedly
**Avoid global transformations for identity preservation.** Use targeted edits (clothing, accessories) instead of scene-wide transforms. Add identity-preserving keywords if needed.
### Processing taking too long
**Optimize your settings:**
* Use FP16 weights instead of FP32
* Reduce resolution if quality allows
* Consider using the API version for faster processing
* Process shorter clips when testing prompts
## Examples showcase the possibilities
### Professional outfit changes
**Input:** Woman in casual wear
**Prompt:** "Change the outfit to a tailored business suit, charcoal gray wool, slim fit blazer, matching pencil skirt, white silk blouse underneath, pearl buttons"
**Result:** Professional transformation while maintaining identity and pose
### Creative character replacements
**Input:** Person walking
**Prompt:** "Replace the person with a steampunk robot, brass gears visible, copper plating, steam vents on shoulders, glowing blue eyes, mechanical joints"
**Result:** Robotic character maintaining original walking motion
### Seasonal scene transformations
**Input:** Summer park scene
**Prompt:** "Transform the scene to autumn, golden hour lighting, orange and red maple leaves falling, warm amber tones, soft shadows, crisp atmosphere"
**Result:** Complete seasonal change with consistent motion
## Additional resources and support
**Get help when you need it.** Join the Decart Discord at [https://discord.gg/decart](https://discord.gg/decart) for community support and updates.
**Report issues on GitHub.** Submit bug reports and feature requests at [https://github.com/DecartAI/lucy-edit-comfyui](https://github.com/DecartAI/lucy-edit-comfyui)
**Access the technical report.** Read the full technical details at [https://d2drjpuinn46lb.cloudfront.net/Lucy\_Edit\_\_High\_Fidelity\_Text\_Guided\_Video\_Editing.pdf](https://d2drjpuinn46lb.cloudfront.net/Lucy_Edit__High_Fidelity_Text_Guided_Video_Editing.pdf)
**Try the web playground.** Experiment without installation at [https://platform.decart.ai](https://platform.decart.ai)
## Latest updates
* **September 17, 2025:** Initial Lucy Edit Dev weights and reference code released
* **September 16, 2025:** Diffusers integration merged (PR #12340)
## Coming soon
* Support for Lucy Edit Dev/Image API
* Enhanced prompt optimization tools
* Batch processing capabilities
# E-commerce Virtual Try-On
Source: https://docs.platform.decart.ai/examples/ecommerce-try-on
Add a 'Try it on' button to product pages with realtime virtual try-on
Let shoppers see how clothing looks on them in realtime, using just a webcam. This walkthrough covers the key integration points for adding virtual try-on to an e-commerce product page — from creating secure client tokens to sending garment images with descriptive prompts.
This example uses Next.js, but the same `@decartai/sdk` works with any JavaScript framework.
**Source code:** [github.com/DecartAI/tryon-examples](https://github.com/DecartAI/tryon-examples) — includes six production-ready examples (e-commerce, standalone, digital mirror, outfit builder, and more).
## What you'll build
A "Try it on" button on product pages that opens a realtime try-on modal with the user's webcam.
Switch between products without reconnecting — each click sends a new garment image and prompt.
Your permanent API key stays on the server. The browser only receives a short-lived ephemeral token.
## Prerequisites
* Node.js 18+
* A Decart API key from [platform.decart.ai](https://platform.decart.ai)
## How it works
The entire integration is three steps:
1. **Server** creates a short-lived client token from your permanent API key
2. **Browser** opens the camera and establishes a WebRTC connection to `lucy-vton-latest`
3. **Browser** sends a garment image + descriptive prompt via `set()` — the model dresses the person in realtime
```mermaid theme={null}
sequenceDiagram
participant S as Your Server
participant D as Decart
participant B as Browser (Decart SDK)
S->>D: POST /v1/client/tokens (API key)
D-->>S: { apiKey: "eyJ...", expiresAt }
S->>B: Pass client token
B->>D: WebRTC connect (camera stream)
D->>B: WebRTC (transformed video)
Note over B,D: set({ image, prompt }) to change outfits
```
## Step 1: Create a client token (server-side)
Your backend creates a short-lived token using your permanent API key. The browser never sees your real key.
```typescript theme={null}
// app/api/tokens/route.ts
import { createDecartClient } from "@decartai/sdk";
import { NextResponse } from "next/server";
export async function POST() {
const client = createDecartClient({
apiKey: process.env.DECART_API_KEY,
});
const token = await client.tokens.create();
return NextResponse.json(token);
}
```
Client tokens expire 60 seconds after minting by default. Let the SDK fetch one for every connect and reconnect with `apiKeyProvider`, as in step 2, rather than minting when the page loads. Active WebRTC sessions continue working after the token expires. See [Token lifetime](/getting-started/client-tokens#token-lifetime).
## Step 2: Connect camera to the realtime model
The frontend opens the camera and establishes a WebRTC connection to `lucy-vton-latest`. The SDK fetches a client token from your backend right before dialling.
```typescript theme={null}
import { createDecartClient, models } from "@decartai/sdk";
// 1. Fetch a client token from your backend right before every connect and reconnect
const client = createDecartClient({
apiKeyProvider: async () => {
const res = await fetch("/api/tokens", { method: "POST" });
const { apiKey } = await res.json();
return apiKey;
},
});
// 2. Open the user's camera
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: "user" },
});
// 3. Connect to the realtime model
const rtClient = await client.realtime.connect(stream, {
model: models.realtime("lucy-vton-latest"),
onRemoteStream: (remoteStream) => {
document.getElementById("output").srcObject = remoteStream;
},
});
```
## Step 3: Send a garment image + prompt
Call `set()` to send a reference garment image with a descriptive prompt. The model applies the garment to the person in realtime.
```typescript theme={null}
await rtClient.set({
prompt: "Substitute the current top with a black bomber jacket with a blue logo on the chest and a zip front",
image: garmentBlob,
enhance: false,
});
```
Call `set()` again at any time to switch garments — no need to reconnect.
## Product catalog pattern
For e-commerce, pre-write prompts for each product in your catalog. This gives the best results since you can craft specific, detailed descriptions.
```typescript theme={null}
interface Product {
name: string;
image: string;
prompt: string;
price: number;
}
const products: Product[] = [
{
name: "Bomber Jacket",
image: "/products/bomber.png",
prompt: "Substitute the current top with a black bomber jacket with a blue logo on the chest and a zip front",
price: 149,
},
{
name: "Knit Beanie",
image: "/products/beanie.png",
prompt: "Add a navy blue knit beanie with a white cross logo on the fold to the person's head",
price: 35,
},
{
name: "Polo Shirt",
image: "/products/polo.png",
prompt: "Substitute the current top with a navy blue polo shirt with a white logo on the chest",
price: 69,
},
];
// When user clicks "Try it on"
async function tryOn(product: Product) {
const garmentBlob = await fetch(product.image).then((r) => r.blob());
await rtClient.set({
prompt: product.prompt,
image: garmentBlob,
enhance: false, // use your pre-written prompt as-is
});
}
```
### Prompt patterns
Use the **substitute** pattern when replacing an existing garment, and the **add** pattern when adding something new:
| Pattern | When to use | Example |
| - | - | - |
| **Substitute** | Replacing an existing garment | `"Substitute the current top with a red plaid flannel shirt with a relaxed fit"` |
| **Add** | Adding something the person isn't wearing | `"Add a wide-brimmed straw hat to the person's head"` |
Be specific — include color, material, texture, pattern, and fit. Aim for **20–30 words**.
```
✅ "Substitute the current top with a bright red hoodie with an oversized casual fit"
✅ "Add a navy baseball cap with a blue logo to the person's head"
❌ "Put a jacket on the person" (too vague)
❌ "Red hoodie" (missing action and context)
```
## Generating prompts for user uploads
For user-uploaded garment images where you don't have a pre-written prompt, use any vision LLM to auto-generate one from the garment image:
```typescript theme={null}
// app/api/enhance-prompt/route.ts
import OpenAI from "openai";
const openai = new OpenAI();
export async function POST(req: Request) {
const formData = await req.formData();
const image = formData.get("image") as File;
const personFrame = formData.get("personFrame") as File | null; // optional
const messages = [
{
role: "system" as const,
content: `You write prompts for a virtual try-on model. Examine the garment image and write a prompt using this pattern:
"Substitute the current top with [detailed garment description]" or "Add [item] to the person's [body part]".
Include color, material, texture, pattern, and fit. 20-30 words. Return only the prompt.`,
},
{
role: "user" as const,
content: [
{ type: "image_url" as const, image_url: { url: await fileToDataUrl(image) } },
...(personFrame
? [{ type: "image_url" as const, image_url: { url: await fileToDataUrl(personFrame) } }]
: []),
],
},
];
const completion = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages,
max_tokens: 100,
});
return Response.json({ prompt: completion.choices[0].message.content });
}
```
Sending a camera frame of the person alongside the garment image gives the LLM context about what they're currently wearing, producing more accurate prompts (e.g. "Substitute the grey sweater" instead of generic "Substitute the current top").
## Reference image tips
* **Clean garment images work best** — just the clothing item, no person wearing it
* **White or plain backgrounds** are ideal
* **At least 512×512 pixels** — the model reproduces what it sees, so a clear image produces better results
* If your product images show a model wearing the garment, consider using an image editing model to extract just the clothing item first
## More examples
The [tryon-examples](https://github.com/DecartAI/tryon-examples) repo includes six production-ready Next.js examples:
| Example | Use case | Highlights |
| - | - | - |
| **E-commerce** | "Try it on" on product pages | Pre-written prompts, modal overlay |
| **Standalone** | Dedicated try-on experience | LLM-generated prompts from garment images |
| **Person Detection** | Kiosks and unattended displays | Auto-connects only when someone is in frame |
| **Full-Featured** | Complete try-on experience | Person detection, clothing extraction, extreme precision |
| **Digital Mirror** | In-store kiosk / smart mirror | Two-device setup (display + phone controller via QR code) |
| **Outfit Builder** | Multi-garment styling | Compose top + bottom garments into full outfits |
## Next steps
Full guide for realtime and batch virtual try-on, including all parameters and SDK examples.
Process pre-recorded videos asynchronously with the batch virtual try-on endpoint.
Secure your integration with short-lived ephemeral tokens.
Optimize your realtime integration for the best experience.
# Examples
Source: https://docs.platform.decart.ai/examples/overview
Official examples hub for realtime-first Decart integrations
Use this as the single entry point for Decart examples. Start with Lucy 2.1 and realtime flows, then branch into batch workflows as needed.
If you're building a new product, start with **Realtime Mobile App** or **Use Cases** first.
## Recommended example paths
End-to-end patterns for realtime and batch products.
Add a "Try it on" button to product pages with realtime virtual try-on.
Full Expo/React Native architecture and WebRTC integration.
Workflow-based editing setup and model usage in ComfyUI.
## GitHub example repositories
Decart maintains multiple public examples, including:
* [`tryon-examples`](https://github.com/DecartAI/tryon-examples) - Six production-ready Next.js virtual try-on examples (e-commerce, digital mirror, outfit builder, and more).
* [`decart-example-expo-realtime`](https://github.com/DecartAI/decart-example-expo-realtime) - Expo app demonstrating Decart realtime video models.
* [`decart-example-expo-greeting`](https://github.com/DecartAI/decart-example-expo-greeting) - Expo app for personalized greeting image generation.
* [`decart-example-outfitters`](https://github.com/DecartAI/decart-example-outfitters) - React + Vite demo for weather-based outfit generation from a photo.
* [`decart-example-telegram-bot`](https://github.com/DecartAI/decart-example-telegram-bot) - Telegram bot integration for Decart image and video models.
Browse these repositories from the DecartAI GitHub organization for production-style implementation references.
# Realtime Mobile App
Source: https://docs.platform.decart.ai/examples/real-time-mobile-app
Build a realtime AI video app with Expo and the Decart SDK
Transform live camera video with AI on mobile devices. This walkthrough covers the architecture and key integration points of an open-source Expo app that connects to Decart's realtime models — including Lucy 2.1 for character transformation.
This example uses Expo with LiveKit's React Native WebRTC stack. React Native realtime requires the LiveKit packages and an early `registerGlobals()` call — plain `react-native-webrtc` is not supported. See [LiveKit setup for React Native](#livekit-setup-for-react-native) below.
**Source code:** [github.com/DecartAI/decart-example-expo-realtime](https://github.com/DecartAI/decart-example-expo-realtime)
## What you'll build
Switch between Lucy Restyle Live, Lucy editing, and — after a quick upgrade — Lucy 2.1 for character transformation.
Swipe through curated looks: anime characters, tuxedos, superhero costumes, and more. Each preset is a text prompt sent to the model.
Toggle front/back camera, switch view modes (fullscreen, picture-in-picture, split), and see your original and transformed feeds side by side.
## Prerequisites
* Node.js 18+
* Expo (`npx expo` — no global install needed)
* A Decart API key from [platform.decart.ai](https://platform.decart.ai)
* A physical iOS or Android device (WebRTC requires real camera hardware)
## Project setup
```bash theme={null}
git clone https://github.com/DecartAI/decart-example-expo-realtime.git
cd decart-example-expo-realtime
```
```bash theme={null}
npm install
```
Create a `.env` file in the project root:
```bash theme={null}
EXPO_PUBLIC_DECART_API_KEY=your-api-key-here
```
```bash theme={null}
npx expo run:ios
# or
npx expo run:android
```
WebRTC requires a physical device. The iOS Simulator and Android Emulator do not support camera streaming.
## LiveKit setup for React Native
React Native realtime runs on [LiveKit's React Native packages](https://github.com/livekit/client-sdk-react-native). Plain `react-native-webrtc` is **not** supported. The cloned example already wires this up — here's what makes it work.
**1. Install the LiveKit packages** (already in the example's `package.json`):
```bash theme={null}
npm install @decartai/sdk livekit-client@2.20.1 \
@livekit/react-native@2.11.1 \
@livekit/react-native-webrtc@144.1.1
```
**2. Register LiveKit's native globals before the app starts.** The SDK reads the WebRTC globals that LiveKit installs, so `registerGlobals()` must run before your router or root component is imported:
```typescript theme={null}
// livekit-bootstrap.ts
import { registerGlobals } from "@livekit/react-native";
registerGlobals();
```
```typescript theme={null}
// index.ts — import the bootstrap before the app entrypoint
import "./livekit-bootstrap";
import "expo-router/entry"; // or your root App component
```
**3. Add the Expo config plugins.** Install them and register them in `app.json`:
```bash theme={null}
npm install @livekit/react-native-expo-plugin@1.0.2 \
@config-plugins/react-native-webrtc
```
```json theme={null}
{
"expo": {
"plugins": [
"@livekit/react-native-expo-plugin",
"@config-plugins/react-native-webrtc"
]
}
}
```
Then run `npx expo prebuild` and rebuild the native app. LiveKit does not run in Expo Go — you need a development build on a physical device.
Using **bare React Native (CLI)** instead of Expo? The realtime code is identical, but you need two Metro/Babel config changes (Expo applies them for you). See [React Native CLI (bare projects)](/sdks/javascript-realtime#react-native-cli-bare-projects).
## Architecture overview
The app uses Expo Router with three screens:
| Screen | File | Purpose |
| - | - | - |
| Entry | `app/index.tsx` | Checks camera/microphone permissions, routes accordingly |
| Permissions | `app/permission-screen.tsx` | Requests camera and microphone access |
| Camera | `app/camera.tsx` | Renders the main `` component |
Key directories:
```
components/camera/
hooks/useWebRTC.ts — Core Decart SDK integration
ui/ModelSelector.tsx — Model toggle UI
ui/VideoRenderer.tsx — Displays local + remote streams
ui/style-carousel/ — Swipeable style picker
livekit-bootstrap.ts — registerGlobals() before the app entrypoint
lib/realtime/
media-streams.ts — Camera capture via LiveKit's mediaDevices
lib/skins/
lucy-skin-list.ts — 21 text prompts for Lucy editing presets
lucy-restyle-live-skin-list.ts — Style presets with optional audio
```
The example reads a permanent API key from `EXPO_PUBLIC_DECART_API_KEY` to keep the walkthrough short. In production, mint [client tokens](/getting-started/client-tokens) on your backend and pass `apiKeyProvider` to `createDecartClient`, so every connect and reconnect dials with a fresh token.
## Core integration: the WebRTC hook
The `useWebRTC` hook handles the entire Decart SDK lifecycle — creating a client, capturing the camera, connecting to a realtime model, and managing cleanup.
```typescript theme={null}
import { useCallback, useRef, useState } from "react";
import { createDecartClient, type ModelDefinition, type RealTimeClient, resolveFpsNumber } from "@decartai/sdk";
import type { MediaStream } from "@livekit/react-native-webrtc";
import { getMediaStream } from "@/lib/realtime/media-streams";
export function useWebRTC({ model, facingMode }: { model: ModelDefinition; facingMode: string }) {
const [localMediaStream, setLocalMediaStream] = useState(null);
const [remoteMediaStream, setRemoteMediaStream] = useState(null);
const [connectionState, setConnectionState] = useState("disconnected");
const realtimeClientRef = useRef(null);
const initializeWebRTC = useCallback(
async ({ model, facingMode }: { model: ModelDefinition; facingMode: string }) => {
try {
// 1. Create the Decart client
const decartClient = createDecartClient({
apiKey: process.env.EXPO_PUBLIC_DECART_API_KEY as string,
});
// 2. Capture camera with model-specific constraints.
// resolveFpsNumber() turns the model's FPS descriptor into a number.
const stream = await getMediaStream(facingMode, {
fps: resolveFpsNumber(model.fps),
width: model.width,
height: model.height,
});
setLocalMediaStream(stream);
setConnectionState("connecting");
// 3. Connect to the realtime model. Pass LiveKit's MediaStream type as the
// generic — no casts — and select VP8 with preferredVideoCodec.
realtimeClientRef.current = await decartClient.realtime.connect(stream, {
model,
preferredVideoCodec: "vp8",
onRemoteStream: (transformedStream) => {
setRemoteMediaStream(transformedStream);
setConnectionState("connected");
},
});
} catch (error) {
console.error("Connection failed:", error);
setConnectionState("failed");
}
},
[],
);
const cleanupWebRTC = useCallback(() => {
realtimeClientRef.current?.disconnect();
setRemoteMediaStream(null);
setConnectionState("disconnected");
}, []);
return {
localMediaStream,
remoteMediaStream,
connectionState,
realtimeClientRef,
initializeWebRTC,
cleanupWebRTC,
};
}
```
Three things to note:
1. **Model constraints drive camera setup.** Each model defines its own `fps`, `width`, and `height`. Resolve the FPS with `resolveFpsNumber(model.fps)` (it can be a descriptor object rather than a plain number), and `getMediaStream` requests exactly those dimensions from LiveKit's `mediaDevices`.
2. **`preferredVideoCodec: "vp8"` selects the codec.** React Native works best with VP8. The SDK negotiates it for you — there's no manual SDP rewriting (the old `customizeOffer` / `forceCodecInSDP` approach is gone).
3. **No `MediaStream` casts.** `connect(...)` takes LiveKit's React Native `MediaStream` type as a generic, so you pass the captured stream straight through — no `as unknown as` bridging.
## Switching models
Each model offers different creative capabilities:
| Model | ID | What it does |
| - | - | - |
| **Lucy 2.1** | `lucy-2.1` | Character transformation and text-guided editing in one model |
| **Lucy Restyle Live** | `lucy-restyle-2` | Full video restyling — transform scenes into different visual styles |
Switch models by creating a new `ModelDefinition` and reconnecting:
```typescript theme={null}
import { models, type ModelDefinition } from "@decartai/sdk";
// Select a model
const restyleLive = models.realtime("lucy-restyle-2");
const lucy2 = models.realtime("lucy-2.1");
// To switch: disconnect, update state, reconnect
async function switchModel(newModel: ModelDefinition) {
cleanupWebRTC();
await new Promise((resolve) => setTimeout(resolve, 100)); // allow cleanup to complete
await initializeWebRTC({ model: newModel, facingMode });
}
```
Use Lucy 2.1 as the default for new mobile integrations. It supports both character reference and text-only editing.
## Applying style presets
The app includes curated prompt presets for each model. When a user selects a preset, the prompt is sent to the active model:
```typescript theme={null}
// Apply a style preset to the current model
if (realtimeClientRef.current) {
realtimeClientRef.current.setPrompt(skin.prompt, {
enhance: skin.enrich ?? false,
});
}
```
Example prompts from the preset list:
```typescript theme={null}
const presets = [
{
title: "Anime Character",
prompt:
"Transform the person into a 2D anime character with smooth cel-shaded lines, " +
"soft pastel highlights, large expressive eyes, clean contours, even lighting, " +
"simplified textures, and a bright studio-style background for a polished anime look.",
},
{
title: "Black Tuxedo",
prompt:
"Change the outfit to a sharp black tuxedo with satin lapels, crisp white shirt, " +
"black bow tie, tailored fit, polished shoes, and soft indoor lighting reflecting " +
"off a marble floor.",
},
{
title: "Sunglasses",
prompt:
"Add a pair of dark tinted sunglasses resting naturally on the person's face, " +
"smooth acetate frames, subtle reflections on the lenses, accurate nose " +
"placement, and soft shadows across the cheeks.",
},
];
```
For Lucy 2.1, use `set()` instead of `setPrompt()` to include a reference image:
```typescript theme={null}
// Lucy 2.1: transform into a character using a reference image
await realtimeClientRef.current.set({
prompt: "Transform into this character",
image: characterImage, // File, Blob, or URL
enhance: true,
});
```
## Handling app lifecycle
Mobile apps frequently move between foreground and background. The example handles this by disconnecting when backgrounded and reconnecting when foregrounded:
```typescript theme={null}
import { AppState, type AppStateStatus } from "react-native";
useEffect(() => {
const handleAppStateChange = (nextAppState: AppStateStatus) => {
if (appStateRef.current.match(/inactive|background/) && nextAppState === "active") {
// App returned to foreground — reconnect
initializeWebRTC({ model, facingMode });
} else if (appStateRef.current === "active" && nextAppState.match(/inactive|background/)) {
// App going to background — disconnect to free resources
cleanupWebRTC();
}
appStateRef.current = nextAppState;
};
const subscription = AppState.addEventListener("change", handleAppStateChange);
return () => subscription?.remove();
}, [cleanupWebRTC, initializeWebRTC]);
```
Always disconnect when backgrounded. WebRTC connections consume battery and network resources even when the app isn't visible.
## Camera controls
**Mirror the front camera.** The SDK's `mirror` option is browser-only and is rejected on React Native with `UNSUPPORTED_PLATFORM_FEATURE`. Mirror the local preview at the view layer instead — pass `mirror` to LiveKit's ``:
```typescript theme={null}
import { RTCView } from "@livekit/react-native-webrtc";
```
**Switch facing mode.** The camera stream needs to be recaptured when toggling between front and back cameras, since each has different hardware capabilities.
## Adding Lucy 2.1 support
Adding Lucy 2.1 requires two small changes: updating the model selector and using the `set()` method for character reference images.
**1. Add Lucy 2.1 to the model selector:**
```typescript theme={null}
import { models } from "@decartai/sdk";
// Add lucy-2.1 alongside the existing models
const handleTabPress = (modelName: "lucy-restyle-2" | "lucy-2.1") => {
const newModel = models.realtime(modelName);
onModelChange(newModel);
};
```
**2. Use `set()` for character reference images:**
Lucy 2.1's key feature is character transformation via reference images. Replace `setPrompt()` with `set()` when a reference image is available:
```typescript theme={null}
// For Lucy 2.1 with a reference image
await realtimeClientRef.current.set({
prompt: "Transform into this character",
image: selectedImage,
enhance: true,
});
// For text-only editing (works with all models)
realtimeClientRef.current.setPrompt("Add sunglasses", { enhance: true });
```
## Next steps
Character transformation, reference images, and the `set()` API in detail.
Full SDK reference for connection management, events, and client-side auth.
Compare Lucy 2.1 and Lucy Restyle Live side by side.
Clone the full example app and start building.
# Use Cases
Source: https://docs.platform.decart.ai/examples/use-cases
What you can build with Decart's realtime and batch AI models
Decart's models power a range of creative and production applications — from realtime character transformation on mobile to batch video processing for content pipelines. This page highlights the most common use cases with recommended models and starter code for each.
## Realtime use cases
Realtime models process live video and audio over WebRTC with sub-second latency. They run continuously as long as the connection is open.
Become any character on camera with Lucy 2.1. Upload a reference image and your movements map onto that identity in realtime.
Apply artistic styles to live video with Lucy Restyle Live. Transform your stream into anime, cyberpunk, oil painting, and more.
Add, modify, or remove objects in live video with Lucy 2.1. Change outfits, add accessories, or swap backgrounds with text prompts.
Let shoppers see how clothing looks on them in realtime. Send a garment image and prompt to Lucy VTON for instant outfit changes.
***
### Character transformation with Lucy 2.1
Turn yourself into any character — live on camera. Lucy 2.1 takes a reference image and maps your facial expressions, head movements, and gestures onto that character.
**Best for:** virtual cosplay, social filters, virtual try-on, character-driven content creation.
```typescript theme={null}
import { createDecartClient, models } from "@decartai/sdk";
const client = createDecartClient({ apiKey: "your-api-key-here" });
const model = models.realtime("lucy-2.1");
const stream = await navigator.mediaDevices.getUserMedia({
video: { frameRate: model.fps, width: model.width, height: model.height },
});
const realtimeClient = await client.realtime.connect(stream, {
model,
onRemoteStream: (transformed) => {
document.getElementById("output").srcObject = transformed;
},
});
// Upload a character reference and transform
const characterPhoto = document.querySelector("input[type=file]").files[0]; // from file input
await realtimeClient.set({
prompt: "Transform into this character",
image: characterPhoto, // File, Blob, or URL
enhance: true,
});
// Switch characters without reconnecting
const anotherPhoto = await fetch("/another-character.jpg").then((r) => r.blob());
await realtimeClient.set({ prompt: "Transform into this character", image: anotherPhoto });
```
Lucy 2.1 also works without a reference image — pass only a `prompt` to `set()` for text-based editing.
### Live streaming effects with Lucy Restyle Live
Transform the entire visual style of a live video feed. Lucy Restyle Live applies artistic styles in realtime — ideal for Twitch, YouTube Live, or TikTok.
**Best for:** social media filters, creative livestreams, virtual events, themed video calls.
```typescript theme={null}
import { createDecartClient, models } from "@decartai/sdk";
const client = createDecartClient({ apiKey: "your-api-key-here" });
const model = models.realtime("lucy-restyle-2");
const stream = await navigator.mediaDevices.getUserMedia({
video: { frameRate: model.fps, width: model.width, height: model.height },
});
const realtimeClient = await client.realtime.connect(stream, {
model,
onRemoteStream: (styled) => {
document.getElementById("output").srcObject = styled;
},
});
// Apply a style
realtimeClient.setPrompt("Studio Ghibli animation style", { enhance: true });
// Switch styles on the fly
realtimeClient.setPrompt("Cyberpunk city with neon lighting");
realtimeClient.setPrompt("Watercolor painting with soft edges");
```
### Realtime video editing with Lucy 2.1
Make targeted edits to live video using text prompts. Add objects, change clothing, swap backgrounds — all without interrupting the stream.
**Best for:** interactive experiences, photo booths, AR-style effects, live content creation.
```typescript theme={null}
import { createDecartClient, models } from "@decartai/sdk";
const client = createDecartClient({ apiKey: "your-api-key-here" });
const model = models.realtime("lucy-2.1");
const stream = await navigator.mediaDevices.getUserMedia({
video: { frameRate: model.fps, width: model.width, height: model.height },
});
const realtimeClient = await client.realtime.connect(stream, {
model,
onRemoteStream: (edited) => {
document.getElementById("output").srcObject = edited;
},
});
// Add objects, change outfits, modify the scene
await realtimeClient.set({ prompt: "Add a small dog running around", enhance: true });
await realtimeClient.set({ prompt: "Change the outfit to a sharp black tuxedo with satin lapels" });
await realtimeClient.set({ prompt: "Replace the background with a beach at sunset" });
```
### Virtual try-on with Lucy VTON
Let shoppers see how clothing looks on them in realtime. Lucy VTON takes a garment reference image and a descriptive prompt, then dresses the person on camera — perfect for e-commerce "Try it on" buttons, digital mirrors, and styling apps.
**Best for:** e-commerce product pages, in-store kiosks, digital mirrors, styling and outfit planning.
```typescript theme={null}
import { createDecartClient, models } from "@decartai/sdk";
const client = createDecartClient({ apiKey: "your-api-key-here" });
const model = models.realtime("lucy-vton-latest");
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: "user" },
});
const realtimeClient = await client.realtime.connect(stream, {
model,
onRemoteStream: (dressed) => {
document.getElementById("output").srcObject = dressed;
},
});
// Send a garment image with a descriptive prompt
const garmentBlob = await fetch("/products/bomber.png").then((r) => r.blob());
await realtimeClient.set({
prompt: "Substitute the current top with a black bomber jacket with a blue logo on the chest and a zip front",
image: garmentBlob,
enhance: false,
});
// Switch garments without reconnecting
const beanieBlob = await fetch("/products/beanie.png").then((r) => r.blob());
await realtimeClient.set({
prompt: "Add a navy blue knit beanie with a white logo on the fold to the person's head",
image: beanieBlob,
enhance: false,
});
```
Use the **substitute** pattern when replacing an existing garment (`"Substitute the current top with..."`) and the **add** pattern for new items (`"Add a hat to the person's head"`). See the [full e-commerce try-on example](/examples/ecommerce-try-on) for a complete integration walkthrough.
***
## Batch use cases
Batch models process pre-recorded video and images asynchronously. Submit a job, poll for completion, then download the result.
Transform and edit existing videos — change styles, replace objects, or modify scenes.
Edit existing images with precise text-driven control.
***
### Video editing
Transform existing videos with text instructions. Change styles, replace objects, or modify entire scenes while preserving the original motion.
**Best for:** post-production, content repurposing, brand transformations, visual effects.
```typescript JavaScript theme={null}
import { createDecartClient, models } from "@decartai/sdk";
import { readFileSync, writeFileSync } from "fs";
const client = createDecartClient({ apiKey: "your-api-key-here" });
const videoBlob = new Blob([readFileSync("input.mp4")], { type: "video/mp4" });
const result = await client.queue.submitAndPoll({
model: models.video("lucy-2.1"),
data: videoBlob,
prompt: "Replace the person's outfit with a medieval knight's armor",
onStatusChange: (job) => console.log(`Status: ${job.status}`),
});
if (result.status === "completed") {
const buffer = Buffer.from(await result.data.arrayBuffer());
writeFileSync("edited.mp4", buffer);
}
```
```bash cURL theme={null}
# Submit a video editing job
curl -X POST https://api.decart.ai/v1/jobs/lucy-2.1 \
-H "X-API-KEY: your-api-key-here" \
-F "data=@input.mp4" \
-F "prompt=Transform into Studio Ghibli animation style"
# Poll for result
curl https://api.decart.ai/v1/jobs/{job_id} \
-H "X-API-KEY: your-api-key-here"
# Download when complete
curl https://api.decart.ai/v1/jobs/{job_id}/content \
-H "X-API-KEY: your-api-key-here" \
--output result.mp4
```
### Image editing
Apply edits to existing images with text instructions.
**Best for:** product mockups, creative assets, visual prototyping, style transfers.
```typescript Image editing theme={null}
import { createDecartClient, models } from "@decartai/sdk";
const client = createDecartClient({ apiKey: "your-api-key-here" });
const result = await client.process({
model: models.image("lucy-image-2"),
data: "https://example.com/room.jpg", // File, Blob, URL, or ReadableStream
prompt: "Change the wall color to deep burgundy, keep everything else the same",
enhance_prompt: true,
});
```
***
## Choosing between realtime and batch
| | **Realtime** | **Batch** |
| - | - | - |
| **Latency** | Sub-second (WebRTC) | Seconds to minutes (async jobs) |
| **Input** | Live camera or video stream | Pre-recorded files |
| **Connection** | Persistent — runs as long as connected | One-shot — submit, poll, download |
| **Pricing** | Per second of connection time | Per second of output video |
| **Best for** | Interactive apps, live streaming, mobile | Post-production, content pipelines, automation |
Realtime and batch models use the same `@decartai/sdk` package. The difference is how you connect — `client.realtime.connect()` for realtime, `client.queue.submitAndPoll()` for video, `client.process()` for images.
## Next steps
Deep dive into character transformation with reference images.
Add a "Try it on" button to product pages with realtime virtual try-on.
Build a complete Expo app with model switching and style presets.
Compare every Decart model — realtime and batch — side by side.
# Coding Agents
Source: https://docs.platform.decart.ai/getting-started/ai-coding-agents
Teach your AI coding agent how to use Decart's APIs
Two ways to give your AI coding agent Decart expertise: **skills** teach it API patterns upfront, and the **MCP server** lets it search Decart's documentation on demand. Use both for best results.
## Agent skill
A skill is a static knowledge file your agent loads at startup. It teaches the agent which API to use, how to authenticate, which models to pick, and what mistakes to avoid.
Run this in your project directory:
```bash theme={null}
npx skills add https://docs.platform.decart.ai
```
The CLI auto-discovers the skill and installs it to your detected coding agents.
Your agent now has context about Decart's Realtime, Queue, and Process APIs. Try asking it:
* "Add live camera effects using Decart's realtime API"
* "Generate a video from a text prompt using Decart"
* "Create a talking avatar that lip-syncs to audio"
| Your agent learns | Details |
| - | - |
| API selection | Which API (Realtime, Queue, Process) fits your use case |
| Model selection | Which model for character transform, style transfer, video gen, image gen |
| Authentication | Server-side API keys vs client-side token generation |
| SDK patterns | Correct code patterns for JavaScript and Python |
| Common mistakes | Camera resolution, API key exposure, cleanup, moderation handling |
```bash theme={null}
npx skills add https://docs.platform.decart.ai -a claude-code
npx skills add https://docs.platform.decart.ai -a cursor
```
```bash theme={null}
npx skills add https://docs.platform.decart.ai -g
```
Skills work with 30+ coding agents including Claude Code, Cursor, GitHub Copilot, Windsurf, and OpenCode. See the [full list](https://skills.sh).
## MCP server
The MCP (Model Context Protocol) server gives your agent a live search tool that queries Decart documentation on demand. Unlike the skill which is loaded once, MCP lets the agent look up specific details — exact parameters, migration guides, changelog entries — as it works.
Add the Decart MCP server to your agent:
```bash theme={null}
claude mcp add decart-docs https://docs.platform.decart.ai/mcp --transport http
```
Add to your `~/.codex/config.toml`:
```toml theme={null}
[mcp_servers.decart-docs]
type = "http"
url = "https://docs.platform.decart.ai/mcp"
```
Add to your `.cursor/mcp.json`:
```json theme={null}
{
"mcpServers": {
"decart-docs": {
"url": "https://docs.platform.decart.ai/mcp"
}
}
}
```
Once connected, your agent can search Decart documentation automatically while generating responses — no need to ask it explicitly.
## Skill vs MCP
| | Skill | MCP |
| - | - | - |
| **How it works** | Pre-loaded context at startup | Live search during conversation |
| **Best for** | API patterns, model selection, gotchas | Exact parameters, examples, latest changes |
| **Setup** | `npx skills add` (one time) | Add MCP config (one time) |
| **Token cost** | Loaded once (\~200 lines) | Per-search, only what's needed |
Use both together. The skill gives your agent the big picture so it picks the right API and model. The MCP server fills in specifics when the agent needs exact parameter details or code examples.
## Prompting tips
Get better results from your agent by being specific about what you're building:
* **Name the API** — "Use the Realtime API" or "Use the Queue API" instead of just "use Decart"
* **Specify the model** — "Use `lucy-2.1` for character transform" rather than letting the agent guess
* **Mention the SDK** — "Using the JavaScript SDK" or "Using Python" so it generates the right syntax
* **Ask for client tokens** — "Make sure to use client tokens for the browser code" for frontend work
* **Reference the platform** — "Check the Decart docs for the exact parameters" triggers the MCP search
# Authentication
Source: https://docs.platform.decart.ai/getting-started/authentication
Secure your API requests with API keys
## Get your API key
Create an account at [platform.decart.ai](https://platform.decart.ai)
Navigate to [API Keys](https://platform.decart.ai/api-keys) and create a new API Key
Copy your API key
## Use your API key
Pass your API key when making requests:
```javascript JavaScript theme={null}
import { createDecartClient } from "@decartai/sdk";
const client = createDecartClient({
apiKey: process.env.DECART_API_KEY
});
```
```python Python theme={null}
from decart import DecartClient
import os
client = DecartClient(api_key=os.getenv("DECART_API_KEY"))
```
```bash cURL theme={null}
# Submit job
JOB_ID=$(curl -s -X POST https://api.decart.ai/v1/jobs/lucy-latest \
-H "X-API-KEY: $DECART_API_KEY" \
-F "data=@video.mp4" \
-F "prompt=Transform to anime style" | jq -r '.job_id')
# Poll status and download when complete
while [ "$(curl -s -H "X-API-KEY: $DECART_API_KEY" \
https://api.decart.ai/v1/jobs/$JOB_ID | jq -r '.status')" != "completed" ]; do sleep 2; done
curl -H "X-API-KEY: $DECART_API_KEY" \
https://api.decart.ai/v1/jobs/$JOB_ID/content --output transformed.mp4
```
## Best practices
Never hardcode API keys in your code. Use environment variables.
* Store keys in environment variables
* Rotate keys regularly
* Use different keys for development and production
## Realtime client-side authentication
For browser and mobile apps, use **client tokens** instead of your permanent key (`dct_...`).
Do not expose permanent API keys in frontend bundles. Always mint short-lived client tokens from your backend.
Create a token from your backend:
```javascript JavaScript theme={null}
import { createDecartClient } from "@decartai/sdk";
const client = createDecartClient({ apiKey: process.env.DECART_API_KEY });
const token = await client.tokens.create(); // 60s TTL, unrestricted
// Or scoped:
// const token = await client.tokens.create({ expiresIn: 300, allowedModels: ["lucy-2.1"] });
```
```python Python theme={null}
from decart import DecartClient
import os
async with DecartClient(api_key=os.getenv("DECART_API_KEY")) as client:
token = await client.tokens.create() # 60s TTL, unrestricted
# Or scoped:
# token = await client.tokens.create(expires_in=300, allowed_models=["lucy-2.1"])
```
```bash cURL theme={null}
curl -X POST https://api.decart.ai/v1/client/tokens \
-H "x-api-key: $DECART_API_KEY" \
-H "Content-Type: application/json" \
-d '{"expiresIn": 300, "allowedModels": ["lucy-2.1"]}'
```
The response's `apiKey` is a signed client token the gateway verifies offline, so requests don't wait on a key lookup. Hand it to your frontend.
### Token lifetime
Client tokens expire **60 seconds** after minting by default (`expiresIn`, 1–3600 s). Mint right before connecting, or pass `apiKeyProvider` to `createDecartClient` so the SDK fetches a fresh token for every connect and reconnect. An expired token is rejected with `TOKEN_EXPIRED` before any socket is opened. See [Token lifetime](/getting-started/client-tokens#token-lifetime).
See [Client Tokens](/getting-started/client-tokens) for the complete backend-to-frontend flow and token rotation strategy.
## Need help?
* [Contact support →](mailto:contact@decart.ai)
# Client Tokens
Source: https://docs.platform.decart.ai/getting-started/client-tokens
Secure realtime authentication for browser and mobile apps
Use client tokens for browser and mobile realtime connections.
Client tokens are short-lived credentials you create on your backend, then pass to your frontend for `client.realtime.connect()` and file uploads.
Never expose your permanent API key (`dct_...`) in client-side code. Use client tokens for all browser and mobile realtime sessions.
## Why client tokens
* Safe to send to browsers and mobile apps
* Short-lived (configurable TTL, 1–3600 seconds, default 60s; see [Token lifetime](#token-lifetime))
* Optional model scoping (restrict which models the key can access)
* Optional origin scoping (restrict which web origins can use the key — browser-enforced)
* Limited scope (cannot create new tokens)
* Signed, so the gateway verifies them offline and connecting doesn't wait on a key lookup
* Expiration blocks new connections, but does not disconnect active realtime sessions
## End-to-end flow
Your backend uses your permanent API key to create a client token.
Return `apiKey` and `expiresAt` from your backend endpoint.
Your frontend passes the token to the SDK, minted right before `connect()` or fetched per dial through `apiKeyProvider`. See [Token lifetime](#token-lifetime).
## Options
All options are optional. Without options, tokens use a 60-second TTL and are unrestricted.
| Parameter | Type | Description |
| - | - | - |
| `expiresIn` | `number` | TTL in seconds (1–3600, default 60) |
| `allowedModels` | `string[]` | Restrict which models the key can access (max 20) |
| `allowedOrigins` | `string[]` | Restrict which web origins can use the key (max 20, see [Origin scoping](#origin-scoping)) |
| `constraints` | `object` | Operational limits (see below) |
| `metadata` | `object` | Custom key-value pairs to attach to the token |
**Constraints object:**
```json theme={null}
{
"realtime": {
"maxSessionDuration": 120 // max seconds per WebSocket session (min 10)
}
}
```
**`expiresIn` vs `maxSessionDuration`** — these control different things.
`expiresIn` sets how long the token can be used to **start** new connections. Once a realtime session is established, the token's expiration does not terminate it.
`maxSessionDuration` caps how long an individual realtime session can remain active, regardless of token expiration.
Use both together for full control: e.g. a 5-minute token window with a 2-minute max per session.
## Token lifetime
Client tokens expire **60 seconds** after minting by default; `expiresIn` sets the TTL from 1 to 3600 seconds. A token minted on page load is usually dead by the time the user has granted camera access and pressed start, and a reconnect with the same token is refused too.
Keep the token fresh in one of two ways:
Pass a function instead of a key. The SDK calls it right before every `connect()`, connect retry, reconnect and `subscribe()`, so each dial carries a token minted for it:
```typescript theme={null}
const client = createDecartClient({
apiKeyProvider: async () => {
const res = await fetch("/api/realtime-token", { method: "POST" }); // your server: client.tokens.create(...)
const { apiKey } = await res.json();
return apiKey;
},
});
const realtimeClient = await client.realtime.connect(stream, { model, onRemoteStream });
```
`apiKeyProvider` wins over `apiKey` for realtime; the HTTP APIs (`process`, `queue`, `files`, `tokens`) still use `apiKey` or `proxy`. A provider rejection during `connect()` rejects the call with that error; during a reconnect it is retried like any other dial failure.
Fetch the token after `getUserMedia` and the user's click, immediately before `connect()`, and size `expiresIn` to your connect window:
```typescript theme={null}
const stream = await navigator.mediaDevices.getUserMedia({
video: { frameRate: model.fps, width: model.width, height: model.height },
});
// Mint now, not on page load.
const res = await fetch("/api/realtime-token", { method: "POST" });
const { apiKey } = await res.json();
const client = createDecartClient({ apiKey });
const realtimeClient = await client.realtime.connect(stream, { model, onRemoteStream });
```
The SDK cannot refresh a static token, so a reconnect after it expires is refused. Prefer `apiKeyProvider` for sessions that need to survive network drops.
### Expired tokens fail fast
Before every realtime dial the SDK reads the token's `exp` claim (5-second clock-skew tolerance) and rejects an expired one with `TOKEN_EXPIRED` without opening a socket:
* `connect()` and `subscribe()` reject with `{ code: "TOKEN_EXPIRED", message, data: { expiresAt, expiredSecondsAgo } }`.
* On a reconnect, the session emits an `error` event with `TOKEN_EXPIRED` and moves to `disconnected` without retrying.
Permanent keys pass through to the server unchanged.
Expiry only gates **new** dials. A realtime session that is already connected keeps running after its token expires. Use `constraints.realtime.maxSessionDuration` to cap how long a session can run.
## Model scoping
Pass `allowedModels` to restrict which models a token can be used with. The bouncer verifies model permissions when the client connects — if the model isn't in the allowed list, the connection is rejected.
Tokens created without `allowedModels` are unrestricted and work with any model.
## Origin scoping
Pass `allowedOrigins` to pin a token to a specific list of web origins. When the token is later used to open a realtime session, the connection is accepted only if the browser-issued WebSocket `Origin` header matches one of the listed origins. Mismatched or missing origins are rejected with close code `1008` and `{"type":"error","error":"Origin not allowed"}`.
Each entry must be a **canonical origin** so it compares byte-for-byte to what browsers send. The mint endpoint enforces this and returns a 400 (with the canonical form in the message) when input doesn't match:
* scheme `http://` or `https://`
* lowercase scheme and host
* no trailing slash, path, query, or fragment
* no userinfo (credentials)
* no default port (`:443` for https, `:80` for http)
* max 253 chars per entry, max 20 entries per token
Examples:
| Input | Outcome |
| - | - |
| `https://app.example.com` | accepted |
| `http://localhost:3000` | accepted (non-default port preserved) |
| `https://app.example.com/` | rejected — trailing slash |
| `https://app.example.com:443` | rejected — default port |
| `https://EXAMPLE.com` | rejected — mixed case |
| `https://user@example.com` | rejected — credentials |
| `example.com` | rejected — missing scheme |
Tokens created without `allowedOrigins` are unrestricted (no origin enforcement).
**Defense-in-depth, not hermetic.** Origin enforcement is browser-driven: it materially raises the cost of stolen-token replay from a different web origin, but does not protect against an attacker who controls a non-browser HTTP client and can spoof the `Origin` header. Treat it as one layer alongside short TTLs and model scoping, not a sole boundary.
## Backend examples
```typescript Next.js Route Handler theme={null}
// app/api/realtime-token/route.ts
import { createDecartClient } from "@decartai/sdk";
import { NextResponse } from "next/server";
const client = createDecartClient({
apiKey: process.env.DECART_API_KEY,
});
export async function POST() {
try {
// Basic — 60s TTL, unrestricted
const token = await client.tokens.create();
return NextResponse.json(token);
} catch {
return NextResponse.json({ error: "Failed to create token" }, { status: 500 });
}
}
```
```typescript Next.js (scoped) theme={null}
// app/api/realtime-token/route.ts
import { createDecartClient } from "@decartai/sdk";
import { NextResponse } from "next/server";
const client = createDecartClient({
apiKey: process.env.DECART_API_KEY,
});
export async function POST() {
try {
// Scoped — 5 min TTL, restricted model + origin, 2 min max session
const token = await client.tokens.create({
expiresIn: 300,
allowedModels: ["lucy-2.1"],
allowedOrigins: ["https://app.example.com"],
constraints: { realtime: { maxSessionDuration: 120 } },
});
return NextResponse.json(token);
} catch {
return NextResponse.json({ error: "Failed to create token" }, { status: 500 });
}
}
```
```python FastAPI theme={null}
from fastapi import FastAPI, HTTPException
from decart import DecartClient
import os
app = FastAPI()
client = DecartClient(api_key=os.environ["DECART_API_KEY"])
@app.post("/api/realtime-token")
async def create_realtime_token():
try:
# Basic — 60s TTL, unrestricted
token = await client.tokens.create()
return {"apiKey": token.api_key, "expiresAt": token.expires_at}
except Exception:
raise HTTPException(status_code=500, detail="Failed to create token")
```
```python FastAPI (scoped) theme={null}
from fastapi import FastAPI, HTTPException
from decart import DecartClient
import os
app = FastAPI()
client = DecartClient(api_key=os.environ["DECART_API_KEY"])
@app.post("/api/realtime-token")
async def create_realtime_token():
try:
# Scoped — 5 min TTL, restricted model + origin, 2 min max session
token = await client.tokens.create(
expires_in=300,
allowed_models=["lucy-2.1"],
allowed_origins=["https://app.example.com"],
constraints={"realtime": {"maxSessionDuration": 120}},
)
return {
"apiKey": token.api_key,
"expiresAt": token.expires_at,
"permissions": token.permissions,
"constraints": token.constraints,
}
except Exception:
raise HTTPException(status_code=500, detail="Failed to create token")
```
## Frontend example
```typescript theme={null}
import { createDecartClient, models } from "@decartai/sdk";
const model = models.realtime("lucy-2.1");
// The SDK calls the provider right before every connect and reconnect, so each
// dial carries a token minted for it. Client tokens expire 60 s after minting.
const client = createDecartClient({
apiKeyProvider: async () => {
const res = await fetch("/api/realtime-token", { method: "POST" });
const { apiKey } = await res.json(); // the short-lived client token, never your permanent key
return apiKey;
},
});
async function connectRealtime() {
const stream = await navigator.mediaDevices.getUserMedia({
video: { frameRate: model.fps, width: model.width, height: model.height },
audio: true,
});
return client.realtime.connect(stream, {
model,
onRemoteStream: (remoteStream) => {
document.getElementById("output").srcObject = remoteStream;
},
initialState: {
prompt: { text: "Anime style", enhance: true },
},
});
}
```
## Rotation strategy
* Mint a token right before each connect, or let `apiKeyProvider` do it for every connect and reconnect. Never mint on page load
* Use shorter TTLs (e.g. 60s) for tighter security; use longer TTLs (e.g. 300–600s) when you mint ahead of connecting
* An expired token is refused before dialling (`TOKEN_EXPIRED`). Mint a new one instead of retrying with the old
* Do not persist client tokens in local storage
* A client token cannot be revoked before `expiresAt`; use a short TTL where fast cut-off matters
* Revoke permanent keys in dashboard if you suspect leakage
For production readiness, also follow the realtime reliability guide in [Streaming Best Practices](/models/realtime/streaming-best-practices).
# Models
Source: https://docs.platform.decart.ai/getting-started/models
Explore Decart's AI models for video and image transformation
Decart offers state-of-the-art AI models for realtime and batch video transformation, lip synchronization, and image editing. Our models are optimized for creative applications, delivering professional-quality results with minimal latency.
**Lucy 2.5 is the latest editing model for realtime and video — text prompts, reference images, or both.** [Get started →](/models/realtime/lucy-2.5)
## Realtime models
Realtime models process live video and audio with low latency. They run continuously as long as the connection is open.
| Model | ID | Resolution | Use Case |
| - | - | - | - |
| **Lucy 2.5** | `lucy-2.5` | 720p | Video editing (latest) |
| **Lucy VTON 3.6** | `lucy-vton-3.6` | 720p | Virtual try-on (latest) |
| **Lucy VTON 3.5** | `lucy-vton-3.5` | 720p | Virtual try-on |
| **Lucy Restyle 2** | `lucy-restyle-2` | 720p | Video restyling |
| **Oasis 3 Preview** | `oasis-3-preview` | 768×512 | Realtime world model / driving simulator |
**Fast mode.** `lucy-2.5` / `lucy-latest`, `lucy-vton-3.6` / `lucy-vton-latest`, and `lucy-vton-3.5` accept `speed: "fast"` at connect time for lower latency and higher throughput, with unchanged output quality. Billed at 2x the standard realtime rate; other models ignore the option. [Learn more →](/models/realtime/lucy-2.5#fast-mode)
**Oasis 3 Preview** is a realtime, promptable world model — you set a scene and drive it with actions, and it generates the next camera frames. It uses the dedicated Python gRPC SDK [`decart-oasis`](https://pypi.org/project/decart-oasis/) rather than the `@decartai/sdk` realtime SDKs. [Get started →](/models/realtime/oasis-3)
| Model | ID | Resolution | Use Case |
| - | - | - | - |
| **Lucy 2.1** | `lucy-2.1` | 720p | Video editing |
## Video models
Video models process pre-recorded video asynchronously. Submit a job, poll for completion, then download the result.
| Model | ID | Resolution | Use Case |
| - | - | - | - |
| **Lucy 2.5** | `lucy-2.5` | 720p | Video editing (latest) |
| **Lucy VTON 3.6** | `lucy-vton-3.6` | 720p | Virtual try-on (latest) |
| **Lucy VTON 3.5** | `lucy-vton-3.5` | 720p | Virtual try-on |
| **Lucy Restyle 2** | `lucy-restyle-2` | 720p | Video restyling |
| Model | ID | Resolution | Use Case |
| - | - | - | - |
| **Lucy 2.1** | `lucy-2.1` | 720p | Video editing |
## Image models
Image models edit and transform images.
| Model | ID | Resolution | Use Case |
| - | - | - | - |
| **Lucy Image 2** | `lucy-image-2` | 480p, 720p | Image editing |
## Latest aliases
Use `-latest` aliases to always target the newest version of a model family. The server resolves these to the current latest version automatically, so your code stays up to date without changes.
| Alias | Type | Currently resolves to |
| - | - | - |
| `lucy-latest` | realtime + video | `lucy-2.5` |
| `lucy-vton-latest` | realtime + video | `lucy-vton-3.6` |
| `lucy-restyle-latest` | realtime + video | `lucy-restyle-2` |
| `lucy-image-latest` | image | `lucy-image-2` |
```typescript theme={null}
// Always use the latest Lucy model — no code changes needed when new versions ship
const model = models.realtime("lucy-latest");
const result = await client.queue.submitAndPoll({
model: models.video("lucy-latest"),
prompt: "Anime style",
data: videoFile,
});
```
## Technical Specifications
### Video Requirements
All video models accept MP4 files with these specifications:
* **Format:** MP4 (H.264 or VP8 codec)
* **Aspect Ratio:** 16:9 or 9:16
* **File Size:** Maximum 200MB
## Choosing the Right Model
### Decision Matrix
| If you need to... | Use this model | Why? |
| - | - | - |
| Transform or edit live video on camera | **Lucy 2.5** (`lucy-2.5`) | Character reference images + text editing in one realtime model (latest) |
| Apply artistic styles to a live stream | **Lucy Restyle 2** (`lucy-restyle-2`) | Full scene restyling in realtime |
| Transform or edit existing video | **Lucy 2.5** (`lucy-2.5`) | Best quality, fastest speed, unlimited duration, supports prompt + reference image (latest) |
| Edit or transform an image | **Lucy Image 2** (`lucy-image-2`) | Precise image editing |
| Simulate a drivable world for RL or robotics | **Oasis 3 Preview** (`oasis-3-preview`) | Realtime world model — actions in, generated camera frames out |
### Quick Start Examples
```typescript Lucy 2.5 (realtime) theme={null}
import { createDecartClient, models } from "@decartai/sdk";
const client = createDecartClient({ apiKey: "your-api-key-here" });
const model = models.realtime("lucy-latest");
const stream = await navigator.mediaDevices.getUserMedia({
video: { frameRate: model.fps, width: model.width, height: model.height },
});
const realtimeClient = await client.realtime.connect(stream, {
model,
onRemoteStream: (s) => { document.getElementById("output").srcObject = s; },
});
// Character transformation
const photo = document.querySelector("input[type=file]").files[0];
await realtimeClient.set({ prompt: "Transform into this character", image: photo, enhance: true });
```
```typescript Lucy Restyle 2 (realtime) theme={null}
import { createDecartClient, models } from "@decartai/sdk";
const client = createDecartClient({ apiKey: "your-api-key-here" });
const realtimeClient = await client.realtime.connect(stream, {
model: models.realtime("lucy-restyle-2"),
onRemoteStream: (s) => { document.getElementById("output").srcObject = s; },
initialState: { prompt: { text: "Cartoon style", enhance: true } },
});
// Switch styles on the fly
realtimeClient.setPrompt("Cyberpunk city");
```
```typescript Lucy 2.5 (batch) theme={null}
import { createDecartClient, models } from "@decartai/sdk";
const client = createDecartClient({ apiKey: "your-api-key-here" });
const result = await client.queue.submitAndPoll({
model: models.video("lucy-latest"),
prompt: "Anime style transformation",
data: videoFile,
resolution: "720p",
});
if (result.status === "completed") {
const buffer = Buffer.from(await result.data.arrayBuffer());
writeFileSync("output.mp4", buffer);
}
```
## Pricing
For detailed pricing information, see the [Pricing page](/getting-started/pricing).
## Next Steps
Character transformation with reference images in realtime
Explore what you can build with realtime and batch models
Transform your first video in minutes
Detailed API documentation
Build with our JavaScript SDK
Optimize your realtime integration
# Overview
Source: https://docs.platform.decart.ai/getting-started/overview
Build with Decart's realtime AI video models. Transform live streams, generate videos, edit images, and animate avatars through a simple API.
Decart provides state-of-the-art AI models for transforming videos and images — both in realtime and as batch jobs — through a simple API.
**New: Lucy 2.5 Realtime** — Edit any live video stream with text prompts, reference images, or both. Add, remove, or replace objects, swap characters, or restyle the whole scene. [Learn more →](/models/realtime/lucy-2.5)
## Get started in 3 steps
Sign up at [platform.decart.ai](https://platform.decart.ai) — you get free credits to start.
```bash theme={null}
npm install @decartai/sdk
```
Also available for [Python](/sdks/python), [Swift](/sdks/swift), and [Android](/sdks/android).
Copy the code, plug in your key, and you'll see transformed video in seconds.
## Try it: realtime character transform
This is the fastest way to see Decart in action. Open your camera, upload a face, and watch yourself become that character live:
```typescript theme={null}
import { createDecartClient, models } from "@decartai/sdk";
const model = models.realtime("lucy-latest");
const stream = await navigator.mediaDevices.getUserMedia({
video: { frameRate: model.fps, width: model.width, height: model.height },
}).catch((err) => { console.error("Camera access failed:", err.message); throw err; });
const client = createDecartClient({ apiKey: "your-api-key-here" });
const realtimeClient = await client.realtime.connect(stream, {
model,
onRemoteStream: (remoteStream) => {
document.getElementById("output").srcObject = remoteStream;
},
onError: (err) => console.error("Connection error:", err),
onDisconnect: (reason) => console.log("Disconnected:", reason),
initialState: {
prompt: {
text: "Substitute the character in the video with the person in the reference image.",
enhance: true,
},
image: referencePhoto, // File, Blob, or URL
},
});
```
See the [Quickstart](/getting-started/quickstart) for full error handling, camera permission flows, and reconnection patterns.
## What you can build
Become any character on camera with Lucy 2.5 — upload a face, see yourself transformed live
Turn any live video stream into anime, cyberpunk, oil paintings, and more via WebRTC
Edit videos with AI
Transform and edit images with text prompts
Add, remove, or modify objects in live video with text prompts
## More examples
Apply artistic styles to a live camera stream:
```typescript theme={null}
import { createDecartClient, models } from "@decartai/sdk";
const model = models.realtime("lucy-restyle-2");
const stream = await navigator.mediaDevices.getUserMedia({
video: { frameRate: model.fps, width: model.width, height: model.height },
}).catch((err) => {
console.error("Camera access failed:", err.message);
throw err;
});
const client = createDecartClient({ apiKey: "your-api-key-here" });
const realtimeClient = await client.realtime.connect(stream, {
model,
onRemoteStream: (s) => {
document.getElementById("output").srcObject = s;
},
onError: (err) => console.error("Connection error:", err),
onDisconnect: (reason) => console.log("Disconnected:", reason),
initialState: {
prompt: { text: "Studio Ghibli animation", enhance: true },
},
});
// Switch styles on the fly
realtimeClient.setPrompt("Cyberpunk city with neon lights");
```
Transform a video file with a single API call:
```typescript theme={null}
import { createDecartClient, models } from "@decartai/sdk";
import { readFileSync, writeFileSync } from "fs";
const client = createDecartClient({ apiKey: process.env.DECART_API_KEY });
const source = new Blob([readFileSync("input.mp4")], { type: "video/mp4" });
const result = await client.queue.submitAndPoll({
model: models.video("lucy-latest"),
data: source,
prompt: "Transform into anime style",
onStatusChange: (job) => console.log(job.status),
});
if (result.status === "completed") {
const buffer = Buffer.from(await result.data.arrayBuffer());
writeFileSync("output.mp4", buffer);
}
```
Edit an image synchronously:
```typescript theme={null}
import { createDecartClient, models } from "@decartai/sdk";
import { readFileSync } from "fs";
const client = createDecartClient({ apiKey: process.env.DECART_API_KEY });
const imageBuffer = readFileSync("input.jpg");
const imageBlob = new Blob([imageBuffer], { type: "image/jpeg" });
const result = await client.process({
model: models.image("lucy-image-2"),
prompt: "Transform into a watercolor painting style",
data: imageBlob,
resolution: "720p",
});
document.querySelector("img").src = URL.createObjectURL(result);
```
[View full quickstart →](/getting-started/quickstart)
## Which API should I use?
Most users start with the **Realtime API** above. If you need batch processing or image editing, here's how the three APIs compare:
| You want to... | API | SDK method |
| - | - | - |
| Transform live camera/video in an interactive app | **Realtime API** | `client.realtime.connect()` |
| Generate or edit videos asynchronously | **Queue API** | `client.queue.submitAndPoll()` |
| Edit images synchronously | **Process API** | `client.process()` |
## Explore
Our flagship realtime model — text and reference-image editing at 720p
Compare all Decart models side by side
Transform your first video in minutes
Complete API documentation
JavaScript, Python, Swift, and Android libraries
Realtime-first starter examples and repo links
Simple pay-as-you-go pricing
# Preset Studio
Source: https://docs.platform.decart.ai/getting-started/preset-studio
Save, organize, share, and version reusable prompts and reference images.
Preset Studio saves a prompt, a reference image, or both as a reusable preset. Use presets in compatible model playgrounds, organize them in folders, share them with an organization, and restore earlier versions.
[Open Preset Studio](https://platform.decart.ai/studio)
Sign in to your Decart account to manage presets. You do not need an API key. Generating a preview uses credits from your account.
## Get started
### Create your first preset in 3 steps
Sign in to [platform.decart.ai](https://platform.decart.ai), then open [Preset Studio](https://platform.decart.ai/studio).
Select **Create Preset**. Add a name and a prompt, a reference image, or both.
Choose a model family, or choose **Any model** to use the preset across supported model playgrounds.
Open a compatible model playground. In the preset picker, select **Mine**, then choose your preset.
The model playground loads the preset's current prompt, reference image, and prompt-enhancement setting.
### What is a preset?
A preset is a reusable set of model inputs. It contains a prompt, a reference image, or both.
A preset can also store:
* A model family and prompt-enhancement setting
* A generated preview
* A folder and visibility setting
* Version history and notes
Applying a preset loads its current saved content. Changes you make afterward in the model playground do not update the preset.
## Create and manage presets
### Ways to create a preset
Create a preset from:
* Preset Studio, by selecting **Create Preset**
* A model playground, by selecting **Save as preset** to reuse its current inputs
* A quickstart template, by saving a copy or customizing it
### Create from scratch
Open [Preset Studio](https://platform.decart.ai/studio) and select **Create Preset**.
Enter a name and add a prompt, a reference image, or both. Reference images can be JPEG, PNG, or WebP files.
Select a model family to limit the preset to compatible model playgrounds. Select **Any model** to make it available across supported model families.
Turn **Enhance prompt** on to expand the saved prompt before generation.
Lucy 2.5 always enhances prompts applied from presets, regardless of the saved setting.
If an organization is active, choose whether the preset is private or shared with that organization.
You can place a model-specific preset in any compatible folder.
Keep **Generate on save** enabled to create a preview when you save, or select **Generate preview** to review one first.
Preview generation requires a prompt.
Optionally add a note for the first version, then select **Create preset**. The preset appears in **Mine**.
Generating or regenerating a preview uses credits. **Generate on save** is enabled by default, but saving unchanged content with a matching preview does not generate another preview or use more credits.
### Start from a quickstart template
Preset Studio includes read-only quickstart templates.
Open a template and choose:
* **Save to my presets** to create a private copy without opening the editor
* **Customize** to open a new preset with the template's prompt, image, model, enhancement setting, and preview
Either path creates an independent copy when you save. Editing your copy does not affect the published template.
If the template has a generated preview, Preset Studio copies it without using additional credits.
### Edit a preset
In **Mine**, select a preset card to open its editor.
| Field | Save behavior |
| - | - |
| Name | Saves when you press Enter or leave the field |
| Visibility and folder | Save immediately |
| Prompt, reference image, model, **Enhance prompt**, and generated preview | Save together as a new version |
To update versioned content:
Change the prompt, reference image, model, or enhancement setting.
Optionally add a version note.
Select **Save new version**. It becomes the current version immediately.
Changes to an organization-shared preset affect every member who uses it.
### Use version history
Every preset begins with a first version. Later content changes create new versions instead of overwriting earlier ones.
The version marked **Current** is applied in model playgrounds.
To reuse an earlier version:
* Select its history row to load it, make any changes, and select **Save new version**.
* Select **Use this version** to make it current without editing it.
Neither action deletes the version it replaces.
You can delete a non-current version when at least one other version remains. Deleting the preset permanently removes the preset and its complete history.
### Preview presets
A generated preview applies the preset to a fixed sample image. If the preset contains a reference image, the reference appears as an inset.
You can:
* Select **Generate preview** to create one before saving.
* Keep **Generate on save** enabled to create one while saving.
* Disable **Generate on save** to preserve the existing preview.
* Regenerate after changing the prompt or reference image.
If preview generation fails while saving, Preset Studio still saves the preset or version and keeps the previous preview when one exists.
## Organize and share presets
### Personal accounts and organizations
The workspace active when you create a preset determines where it belongs.
| Context | Visibility | Who can access the preset |
| - | - | - |
| Personal | Private | Only you |
| Organization, private | Private | Only you, within that organization |
| Organization, shared | Organization | Every member of that organization |
Your personal presets remain in **Mine** while an organization is active. Switching organizations changes which organization presets and folders are available.
#### Share a personal preset with an organization
Make the target organization active.
In **Mine**, select the personal preset.
Change **Visibility** from **Private** to the organization. This moves the preset into that organization and shares it with every member.
If its current folder is not compatible with the new organization or visibility, the preset moves to **General**.
#### Organization permissions
Every member of an organization can view, apply, rename, edit, version, roll back, and delete its shared presets. Only the preset creator can change visibility.
Deleting an organization-shared preset permanently deletes it and its history for every member.
#### Share a link
Select **Copy link** on an organization-shared preset to share it with another member.
Any member of that organization can open the link, even when a different organization is active. People outside the organization cannot open it.
If the creator account is deleted, the shared preset remains available. Its creator appears as **Deleted user**, and its visibility can no longer be changed.
### Folders
Folders group the presets in **Mine** by model family.
Select **Mine** in Preset Studio.
Select the new-folder button beside **Folders**.
Enter a name, choose a model family, and, when an organization is active, choose private or organization visibility.
Open or create a preset with the same model family, then select the folder from its **Folder** field.
Folder rules:
* Folders support one level and cannot be nested.
* Every folder belongs to one model family. Any-model presets cannot be placed in a folder.
* **General** is the default folder and a reserved name. It cannot be renamed or deleted.
* Folder names must be unique within the same model and workspace.
* An organization-shared preset can only use a shared folder from the same organization.
* You can rename or delete a folder, but cannot change its model or visibility after creation.
* Deleting a folder moves its presets to **General**; it does not delete them.
## Find and use presets
### Browse Preset Studio
| Tab | Contents |
| - | - |
| **All presets** | Your available presets and quickstart templates in one flat view |
| **Mine** | Your personal presets and presets available from the active organization |
The folder tree appears when you browse quickstart templates or **Mine**. On smaller screens, it becomes a dropdown.
Search spans the selected tab, not only the selected folder. It matches preset names and prompts, plus template descriptions.
When an organization is active, filter **Mine** by **Private** or **Organization** visibility.
### Apply a preset in a model playground
Compatible model playgrounds let you browse all presets, quickstart templates, or your saved presets.
Selecting a saved preset loads:
* Its current prompt
* Its current reference image
* Its prompt-enhancement setting
* Any-model presets alongside presets for the current model family
Select **Manage in Studio** from **Mine** to edit, organize, or delete presets.
Selecting an image-only preset does not replace text already entered in the model playground.
## Troubleshooting
Check that you are signed in, the correct organization is active, the preset's model matches the model playground, and **Mine** or **All presets** is selected.
Choose **Any model** if the preset should appear across model families.
Preset Studio lists the 500 most recently created presets available in your current context. If you exceed that limit, the library reports that older presets are not listed.
Choose a model family first; any-model presets cannot be placed in folders.
The folder must use the same organization context and model family. Organization-shared presets require a shared folder.
Select or join an organization first. Without an active organization, new presets are private.
Confirm that the preset is shared with the intended organization and that the person is a member.
A private preset is visible only to its creator, even when it belongs to an organization.
The original folder is unavailable or incompatible. It may be private, belong to another organization, or use a different model family.
Move the preset to a compatible shared folder.
Add a prompt. Generated previews require one.
Open **Mine** in a desktop-sized window. Hover over or focus the custom folder in the left sidebar, then select the **•••** menu.
Folder actions are unavailable in **All presets**, the quickstart templates, and the narrow-screen folder dropdown. You also cannot rename or delete **General**, the permanent default folder.
A new name cannot be blank, longer than 100 characters, **General**, or the same as another folder for that model and workspace.
Deleting a custom folder moves its presets to **General**; it does not delete them.
## Next steps
Create and manage presets.
Use prompts and reference images for realtime editing.
Apply saved clothing and styling presets with Lucy VTON.
Compare model families.
# Pricing
Source: https://docs.platform.decart.ai/getting-started/pricing
Clear, usage-based pricing for Decart models
Decart uses pay-as-you-go pricing.
No subscriptions and no minimum spend. You pay only for usage.
## How pricing works
* Realtime models: billed per second of active generation
* Video models: billed per generated second
* Image models: billed per generation
## Realtime models
| Model | ID | 720p | Fast mode | Best for |
| - | - | - | - | - |
| **Lucy 2.5** | `lucy-2.5` | \$0.02/sec | \$0.04/sec | Realtime video editing (latest) |
| **Lucy VTON 3.6** | `lucy-vton-3.6` | \$0.02/sec | \$0.04/sec | Virtual try-on (latest) |
| **Lucy VTON 3.5** | `lucy-vton-3.5` | \$0.02/sec | \$0.04/sec | Virtual try-on |
| **Lucy Restyle 2** | `lucy-restyle-2` | \$0.01/sec | - | Realtime style transfer |
| **Oasis 3 Preview** | `oasis-3-preview` | \$0.02/sec | - | Realtime world model / driving simulator |
**Fast mode** (`speed: "fast"`) gives the session lower latency and higher throughput; output quality is unchanged. It is available for `lucy-2.5` / `lucy-latest`, `lucy-vton-3.6` / `lucy-vton-latest`, and `lucy-vton-3.5`, and is billed at 2x the standard realtime rate for those models. Other models ignore the option. Omit it (the default) for standard mode. See [Fast mode](/models/realtime/lucy-2.5#fast-mode).
Oasis 3 Preview is billed at **\$0.02 per second**, with enterprise pricing available depending on the use case — contact `contact@decart.ai`.
| Model | ID | 720p | Best for |
| - | - | - | - |
| **Lucy 2.1** | `lucy-2.1` | \$0.02/sec | Realtime video editing |
## Video models
| Model | ID | 480p | 720p | Best for |
| - | - | - | - | - |
| **Lucy 2.5** | `lucy-2.5` | - | \$0.04/sec | Video editing (latest) |
| **Lucy VTON 3.6** | `lucy-vton-3.6` | - | \$0.04/sec | Virtual try-on (latest) |
| **Lucy VTON 3.5** | `lucy-vton-3.5` | - | \$0.04/sec | Virtual try-on |
| **Lucy Restyle 2** | `lucy-restyle-2` | - | \$0.01/sec | Video restyling |
| Model | ID | 480p | 720p | Best for |
| - | - | - | - | - |
| **Lucy 2.1** | `lucy-2.1` | - | \$0.04/sec | Video editing |
## Image models
| Model | ID | 480p | 720p | Best for |
| - | - | - | - | - |
| **Lucy Image 2** | `lucy-image-2` | \$0.01 | \$0.02 | Image editing |
## Cost examples
* 5-second video edit with `lucy-2.5` (720p): `5 x $0.04 = $0.20`
* 30-second realtime Lucy 2.5 session: `30 x $0.02 = $0.60`
* 30-second realtime Lucy 2.5 session in fast mode (`speed: "fast"`): `30 x $0.04 = $1.20`
* One 720p image edit with `lucy-image-2`: `$0.02`
* 60-second Oasis 3 Preview session: `60 x $0.02 = $1.20`
## FAQ
New accounts receive free credits so you can test the API before paying.
Video jobs are billed per generated second. Realtime is billed per active generation time.
Yes. Contact `contact@decart.ai` for custom volume pricing.
## Next steps
Pick the best quality/speed/cost tradeoff for your product.
Create an account and run your first request.
# Quickstart
Source: https://docs.platform.decart.ai/getting-started/quickstart
Go from API key to first Decart output in minutes
Get your first successful Decart result in under 5 minutes.
## Prerequisites
* A Decart account at [platform.decart.ai](https://platform.decart.ai)
* An [API key](/getting-started/authentication)
```bash theme={null}
export DECART_API_KEY="your-api-key-here"
```
Using an AI coding agent? Run `npx skills add https://docs.platform.decart.ai` to teach it Decart's APIs, or [connect the MCP server](/getting-started/ai-coding-agents) for live docs search.
## Pick your first integration path
### 1) Install SDK
```bash theme={null}
npm install @decartai/sdk
```
### 2) Connect camera + model
```typescript theme={null}
import { createDecartClient, models } from "@decartai/sdk";
const model = models.realtime("lucy-latest");
// Request camera access — will throw if the user denies permission
let stream: MediaStream;
try {
stream = await navigator.mediaDevices.getUserMedia({
video: { frameRate: model.fps, width: model.width, height: model.height },
audio: true,
});
} catch (err) {
if (err instanceof DOMException && err.name === "NotAllowedError") {
console.error("Camera permission denied. Please allow camera access and reload.");
} else {
console.error("Could not access camera:", err);
}
throw err;
}
const client = createDecartClient({ apiKey: process.env.DECART_API_KEY });
const realtimeClient = await client.realtime.connect(stream, {
model,
mirror: "auto",
onRemoteStream: (remoteStream) => {
document.getElementById("output").srcObject = remoteStream;
},
onError: (err) => {
console.error("Realtime connection error:", err);
},
onDisconnect: (reason) => {
console.log("Disconnected:", reason);
},
initialState: {
prompt: { text: "Substitute the character in the video with an anime-style hero with spiky silver hair and glowing blue eyes.", enhance: true },
},
});
// Switch to a different transformation on the fly
await realtimeClient.setPrompt("Substitute the character in the video with a Renaissance oil painting portrait.");
```
### Expected result
You should see the transformed stream rendered in your output video element.
**Common issues:**
* **Camera permission denied** — the browser will prompt the user. If denied, `getUserMedia` throws a `NotAllowedError`.
* **No camera found** — throws a `NotFoundError`. Check that a video device is connected.
* **Connection drops** — the `onDisconnect` callback fires. In production, implement reconnection logic.
For browser/mobile production setups, use [Client Tokens](/getting-started/client-tokens) instead of exposing a permanent API key. They expire after 60 seconds by default, so fetch them per dial with `apiKeyProvider` rather than on page load.
### 1) Submit + poll with the SDK
```typescript theme={null}
import { createDecartClient, models } from "@decartai/sdk";
import { writeFileSync } from "fs";
const client = createDecartClient({ apiKey: process.env.DECART_API_KEY });
const result = await client.queue.submitAndPoll({
model: models.video("lucy-latest"),
data: "https://example.com/input_video.mp4",
prompt: "Transform into Studio Ghibli animation style",
onStatusChange: (job) => console.log(`Status: ${job.status}`),
});
if (result.status === "completed") {
const buffer = Buffer.from(await result.data.arrayBuffer());
writeFileSync("transformed_video.mp4", buffer);
}
```
### 2) Same flow in multiple languages
```typescript JavaScript SDK theme={null}
import { createDecartClient, models } from "@decartai/sdk";
const client = createDecartClient({ apiKey: process.env.DECART_API_KEY });
const result = await client.queue.submitAndPoll({
model: models.video("lucy-latest"),
data: "https://example.com/input_video.mp4",
prompt: "Transform into Studio Ghibli animation style",
onStatusChange: (job) => console.log(job.status),
});
```
```python Python SDK theme={null}
import asyncio
import os
from decart import DecartClient, models
async def main():
async with DecartClient(api_key=os.getenv("DECART_API_KEY")) as client:
result = await client.queue.submit_and_poll({
"model": models.video("lucy-latest"),
"data": "https://example.com/input_video.mp4",
"prompt": "Transform into Studio Ghibli animation style",
"on_status_change": lambda job: print(job.status),
})
asyncio.run(main())
```
```bash cURL theme={null}
JOB_ID=$(curl -s -X POST https://api.decart.ai/v1/jobs/lucy-latest \
-H "X-API-KEY: $DECART_API_KEY" \
-F "data=@input_video.mp4" \
-F "prompt=Transform into Studio Ghibli animation style" | jq -r '.job_id')
while true; do
STATUS=$(curl -s -H "X-API-KEY: $DECART_API_KEY" \
https://api.decart.ai/v1/jobs/$JOB_ID | jq -r '.status')
[ "$STATUS" = "completed" ] && break
[ "$STATUS" = "failed" ] && exit 1
sleep 2
done
curl -H "X-API-KEY: $DECART_API_KEY" \
https://api.decart.ai/v1/jobs/$JOB_ID/content --output transformed_video.mp4
```
### Expected result
You should have `transformed_video.mp4` in your current directory.
```typescript theme={null}
import { createDecartClient, models } from "@decartai/sdk";
import { readFileSync } from "fs";
const client = createDecartClient({ apiKey: process.env.DECART_API_KEY });
const imageBuffer = readFileSync("input.jpg");
const imageBlob = new Blob([imageBuffer], { type: "image/jpeg" });
const result = await client.process({
model: models.image("lucy-image-2"),
prompt: "Transform into a watercolor painting style",
data: imageBlob,
resolution: "720p",
});
document.querySelector("img").src = URL.createObjectURL(result);
```
## Next steps
Start with the flagship realtime model for character transformation.
Full WebRTC integration guide.
Secure browser/mobile realtime authentication.
Pick the best model for your use case.
Browse official examples and realtime-first starter projects.
See complete realtime and batch product patterns.
# Network Requirements
Source: https://docs.platform.decart.ai/integrations/network-requirements
Network, device, and firewall configuration for running the realtime SDK — for IT, networking, and security teams
This page specifies the network, device, and security configuration required to operate the Decart realtime streaming SDK. It is intended for the IT, networking, and security teams responsible for the deployment environment, and can be shared with an internet service provider where connectivity changes are required.
Updated for SDK v0.1.22 and production infrastructure as of August 2026.
## Supported operating systems
The SDK does not hard-enforce a minimum OS or browser version. Device support is inherited from the underlying WebRTC stack, which transpiles broadly and checks capability at runtime rather than gating on a version number. The values below are recommended minimums for a reliable realtime experience — not hard cutoffs.
| Platform | Recommended | Technical floor |
| - | - | - |
| iOS / iPadOS | iOS / iPadOS 15 or later | iOS 14.3 |
| Android | Android 10 or later with the latest Chrome (or another up-to-date Chromium browser) | Android 8 with Chrome 110 |
| Desktop | Latest stable Chrome, Edge (Chromium), Firefox, or Safari | Chrome 110, Edge (Chromium) 110, Firefox 110, Safari 16 |
On Android, Chrome updates independently of the OS, so a recent Chrome version matters more than the Android version number itself.
## Hardware requirements
The SDK processes bidirectional realtime video at \~30 fps. Sustained performance under continuous load matters more than peak spec — a device can pass these numbers and still throttle after a few minutes of thermal load.
| Component | Minimum | Recommended |
| - | - | - |
| Processor | Dual-core 2.0 GHz | Quad-core 2.5 GHz |
| GPU / video acceleration | Integrated GPU; hardware video acceleration strongly recommended | Hardware H.264/VP9/AV1 encode + decode |
| RAM (free at session start) | 2 GB | 4 GB+ |
| Architecture | 64-bit x86 or ARM | 64-bit x86 or ARM (Apple Silicon supported) |
## Network requirements
### Bandwidth per active session
Each live session is bidirectional and roughly symmetric — you send your camera upstream and receive the transformed video downstream at the same time, at 30 fps. A session typically consumes **1.3–2.4 Mbps in each direction simultaneously** in production measurements.
* **Minimum:** 3.5 Mbps per direction (the bitrate cap per stream).
* **Provision for:** 4 Mbps per direction — both upload and download — per concurrent session, which covers the cap plus protocol overhead.
### Fixed locations
For deployments where the experience must stay up consistently (for example, an in-store installation), we recommend:
* **Wired Ethernet over Wi-Fi** for the device running the session. Wi-Fi introduces packet loss and jitter that cause video freezes.
* **\~2× bandwidth headroom** over the per-session requirement (about 8 Mbps per direction, per session).
* **A business or dedicated line** if the existing connection cannot guarantee stability.
### Mobile users
Mobile reception is variable and cannot be guaranteed. The recommended pattern is to gate the feature on measured connectivity — call `checkConnectivity()` before offering the experience and only show it when the connection is good enough. See [Testing connectivity](#testing-connectivity-with-the-js-sdk).
## Connection quality thresholds
These are the exact thresholds the SDK uses to compute its live connection-quality verdict. The overall verdict is the worst dimension, smoothed over a rolling window with a short warm-up and hysteresis so it does not flicker.
| Metric | Good | Fair | Poor | Critical |
| - | - | - | - | - |
| RTT latency | ≤ 150 ms | ≤ 300 ms | ≤ 500 ms | > 500 ms |
| Packet loss | ≤ 0.1% | ≤ 1% | ≤ 5% | > 5% |
| Inbound frame rate | ≥ 20 fps | ≥ 12 fps | ≥ 5 fps | \< 5 fps |
## Quick readiness check
Run this once per fixed location, on the actual device and network:
Start a real session and let it run 2–3 minutes.
Confirm the connection-quality verdict holds at `"good"` (or at worst `"fair"`) for the whole run — monitor it with the `onConnectionQuality` callback shown in [In-session monitoring](#in-session-warn-or-degrade-if-reception-drops).
Watch for any `videoStall` events or `limitingFactor === "bandwidth"` — either one means the network is not ready.
```typescript theme={null}
rt.on("diagnostic", (event) => {
if (event.name === "videoStall") {
console.log(event.data.stalled ? "stalled" : "recovered", event.data.durationMs);
}
});
```
If both Wi-Fi and wired are available, test both and prefer wired.
## Firewall and network configuration
### Domains to allow (production)
| Domain | Purpose | Protocol |
| - | - | - |
| `api.decart.ai` | REST API, authentication, session setup, realtime signaling (WebSocket) | HTTPS and WSS / TCP 443 |
| `platform.decart.ai` | SDK telemetry, authentication, billing | HTTPS / TCP 443 |
| `lk.decart.ai` | Realtime media signaling (US-West) | WSS / TCP 443 |
| `*.lkc.decart.ai` | Realtime media signaling, regional edge servers | WSS / TCP 443 |
| `stun.l.google.com` | STUN, used by the pre-connect connectivity check | UDP 19302 |
`*.lkc.decart.ai` is required, not optional. Sessions are routed to the nearest regional media server based on client geography, worldwide. A wildcard entry is necessary because the specific hostname a session receives is assigned at connect time. Allowlisting only `lk.decart.ai` will work in US-West and fail everywhere else.
Regional media servers worldwide are served as `.lkc.decart.ai` (for example `eu-west-1.lkc.decart.ai`), and new regions are added regularly — allow the wildcard rather than individual hostnames.
Staging environments use `api.stage-decart.com` and `livekit.stage-decart.com`.
### Ports and protocols
| Port / protocol | Direction | Purpose |
| - | - | - |
| TCP 443 | Outbound | HTTPS and secure WebSocket (WSS) signaling. Always required. |
| UDP 7882 | Outbound | WebRTC media. Required. |
| UDP 3478 | Outbound | TURN relay, used when a direct path is unavailable. |
| UDP 19302 | Outbound | STUN, for the pre-connect connectivity check. |
* **Media-server IPs are dynamic.** Allow by protocol and port, not by fixed destination IP. This applies to UDP 3478 as well: the TURN relay is advertised to the client as a per-session server IP, so there is no single TURN hostname or address to allowlist.
* **DTLS/SRTP:** media is encrypted. Do not attempt to inspect it.
### UDP traffic and proxy configuration
This is the most common cause of "it connects but the video freezes" problems:
* WebRTC prefers UDP (port 7882). Allow outbound UDP where possible.
* Do not force media through an HTTP/HTTPS proxy that cannot pass UDP if you want the low-latency path.
* For SNI / transparent proxies: allow the domains above, and do not TLS-intercept (decrypt) `turn.decart.ai`; doing so breaks the connection.
## Testing connectivity with the JS SDK
The JS SDK exposes two connectivity signals — a quality verdict (`good | fair | poor | critical`) and a limiting factor (`bandwidth | latency | loss | stall | cpu | none`).
### Pre-connect: decide whether to show the feature
Checks whether WebRTC can leave the network over UDP, and measures path latency:
```typescript theme={null}
const { quality, metrics, reasons } = await client.realtime.checkConnectivity();
// quality: "good" | "fair" | "poor" | "critical"
// metrics.transport: "udp" | "relay" | "failed"
if (quality === "critical") showFallback(reasons);
```
### In-session: warn or degrade if reception drops
A smoothed live verdict based on the SDK's WebRTC stats, using the [thresholds above](#connection-quality-thresholds):
```typescript theme={null}
const rt = await client.realtime.connect(stream, {
model,
onRemoteStream: (remoteStream) => {
videoElement.srcObject = remoteStream;
},
onConnectionQuality: ({ quality, limitingFactor }) => {
updateSignalBars(quality);
if (quality === "poor") warnUser(limitingFactor);
},
});
```
## Next steps
Camera setup, connection management, and error handling
Full reference for the connectivity and diagnostics APIs
# Realtime Integration Paths
Source: https://docs.platform.decart.ai/integrations/overview
Choose how to connect your platform to Decart's realtime AI models
Integrate Decart's realtime video AI models — Lucy Restyle Live and Lucy — into your platform and offer them to your users. Choose from multiple integration paths based on how much control you need over the end-user experience.
## Integration paths
Decart realtime models use **WebRTC** for media (video and audio) and a **control plane** for signaling, prompts, and session events. Each integration path differs in how you connect to the control plane — media quality is identical across all paths.
| Path | How it works | White-label | Provider visibility | Client requirements | Your infrastructure |
| - | - | - | - | - | - |
| [SDK Direct](/integrations/sdk-direct) | Your app embeds the Decart SDK | No | Session events only | WebRTC + Decart SDK | Token minting only |
| [WS Signaling Proxy](/integrations/signaling-proxy-ws) | You proxy WebSocket signaling, WebRTC stays direct | Yes | Full | WebRTC (no SDK) | WS proxy server |
| [HTTP Signaling](/integrations/signaling-proxy-http) | You proxy stateless HTTP calls, WebRTC stays direct | Yes | Full | WebRTC (no SDK) | Stateless HTTP proxy |
## Choosing a path
**Do you need white-label?** If it's OK for your users to connect to Decart endpoints directly (visible in network traffic) and depend on the Decart SDK, use [SDK Direct](/integrations/sdk-direct) — it's the simplest path with zero infrastructure overhead.
**If you do need white-label**, choose based on your infrastructure: if your gateway is HTTP-native (most API platforms), use [HTTP Signaling](/integrations/signaling-proxy-http). If you already run WebSocket infrastructure, use [WS Signaling Proxy](/integrations/signaling-proxy-ws). Both deliver identical media quality.
## Authentication
All integration paths use the same authentication system. You receive a Decart API key for your platform and use it to authenticate requests on behalf of your users.
* **Permanent keys** are used server-side to authenticate with Decart
* **Client tokens** are short-lived keys for browser/mobile use — see [Client Tokens](/getting-started/client-tokens)
* Usage is tracked per API key, so you get full visibility into your users' consumption
Your API key is scoped to your platform. Decart tracks usage per key, so you can query consumption and build your own billing on top.
## Next steps
Simplest path — embed the Decart SDK in your app
White-label with WebSocket proxy
White-label with stateless HTTP proxy
# SDK Direct
Source: https://docs.platform.decart.ai/integrations/sdk-direct
Embed the Decart SDK in your app for the simplest integration path
Your application embeds the Decart SDK directly. End users connect to Decart for both signaling (WebSocket) and media (WebRTC). You handle authentication and billing on your side.
This is the simplest integration path — no proxy infrastructure, no protocol translation, full media quality.
## When to use this path
* You want the fastest integration with zero infrastructure overhead
It's OK for end users to connect to Decart endpoints directly (visible in network traffic)
* You're building a wrapper app, marketplace listing, or plugin that surfaces Decart models
If you already use Decart's batch models (video editing, image editing), SDK Direct for realtime works the same way — same API key, same SDK, same billing.
## Characteristics
| Property | Value |
| - | - |
| White-label | No — end users see Decart endpoints and SDK |
| Frame access | No |
| Provider visibility | Session events only (`generationTick`, `connectionChange`) |
| Client requirements | Browser or native app with WebRTC + Decart SDK |
| Your infrastructure | Backend for token minting only |
Media quality is identical to using Decart directly. There is no degradation — the SDK connects straight to Decart.
## Architecture
```mermaid theme={null}
sequenceDiagram
participant B as Your Backend
participant D as Decart
participant F as Your Frontend (Decart SDK)
B->>D: POST /v1/client/tokens (your API key)
D-->>B: { apiKey: "eyJ...", expiresAt }
B->>F: Pass client token
F->>D: WS connect (signaling)
F->>D: WebRTC media (P2P)
D->>F: WebRTC media (P2P)
Note over F,D: Media flows p2p over WebRTC
```
## How it works
Use your platform API key to mint a short-lived client token. This keeps your permanent key server-side.
```typescript theme={null}
// Your backend
const client = createDecartClient({ apiKey: process.env.DECART_API_KEY });
const clientToken = await client.tokens.create(); // expires in 60 s by default
// Return clientToken.apiKey to your frontend
```
Your frontend hands the SDK a function that fetches the client token; it runs before every connect and reconnect, so no dial uses a stale token. The SDK handles WebSocket signaling, WebRTC negotiation, ICE candidates, and auto-reconnection.
```typescript theme={null}
import { createDecartClient, models } from "@decartai/sdk";
const model = models.realtime("lucy-2.1");
const stream = await navigator.mediaDevices.getUserMedia({
video: { frameRate: model.fps, width: model.width, height: model.height },
});
// Fetched right before every connect and reconnect; client tokens expire after 60 s.
const client = createDecartClient({
apiKeyProvider: async () => {
const res = await fetch("/api/realtime-token", { method: "POST" }); // your backend route from step 1
const { apiKey } = await res.json();
return apiKey;
},
});
const rt = await client.realtime.connect(stream, {
model,
onRemoteStream: (remoteStream) => {
videoElement.srcObject = remoteStream;
},
});
await rt.set({ prompt: "Anime style", enhance: true });
```
Use `set()` to replace the session state at any time. Include all fields you want to keep — omitted fields are cleared:
```typescript theme={null}
// Set a new prompt (clears any previous image)
await rt.set({ prompt: "Cyberpunk neon city" });
// Set a reference image with a prompt
await rt.set({
prompt: "Transform into this character",
image: selectedFile, // File, Blob, base64 string, or null to clear
});
// Disable prompt enhancement for exact control
await rt.set({ prompt: "Exact prompt text", enhance: false });
```
Track session state and usage with event listeners:
```typescript theme={null}
rt.on("connectionChange", (state) => {
// "connecting" | "connected" | "generating" | "disconnected" | "reconnecting"
updateStatusIndicator(state);
});
rt.on("generationTick", ({ seconds }) => {
// Total elapsed generation time (cumulative, not a delta)
updateUsageDisplay(seconds);
});
rt.on("error", (error) => {
// DecartSDKError with message and context
console.error("Decart error:", error.message);
});
```
Clean up the session when the user leaves. This closes the WebSocket and WebRTC connections.
```typescript theme={null}
rt.disconnect();
```
## API reference
### `connect(stream, options)`
Creates a new realtime session and establishes the WebRTC connection.
```typescript theme={null}
const rt = await client.realtime.connect(stream, {
model: models.realtime("lucy-2.1"),
onRemoteStream: (stream) => { videoElement.srcObject = stream; },
initialState: {
prompt: { text: "Anime style", enhance: true },
image: selectedFile,
},
});
```
The realtime model — use `models.realtime("model_id")`.
Called when the remote video/audio stream is ready.
Pre-configure a prompt and/or image before the first frame.
Preferred local publish codec. Desktop Safari is always pinned to `vp8`. On React Native, use `"vp8"`.
The `initialState` option lets you set a prompt and image in the same round-trip as session creation — the model starts generating with your configuration from the first frame.
### `set(input)`
Replace the session state with a new prompt, reference image, or both. Fields not included are cleared. At least one of `prompt` or `image` is required. Awaits server acknowledgment — throws if rejected by moderation or on timeout.
```typescript theme={null}
await rt.set({ prompt: "Anime style", enhance: true, image: file });
```
The text prompt to apply.
Enhance the prompt automatically. Set to `false` for exact prompt text.
Reference image. Pass `null` to clear the current image. Strings are treated as base64.
### `setPrompt(prompt, options?)`
Set the prompt and wait for server acknowledgment. Throws if the prompt is rejected by moderation or the request times out (15 seconds).
```typescript theme={null}
try {
await rt.setPrompt("Anime style", { enhance: true });
} catch (error) {
// Prompt was rejected by moderation or timed out
console.error(error.message);
}
```
Enhance the prompt automatically.
### `setImage(image, options?)`
Set a reference image and wait for server acknowledgment. Throws if the image is rejected by moderation or the request times out.
```typescript theme={null}
try {
await rt.setImage(selectedFile, {
prompt: "Transform into this character",
enhance: true,
});
} catch (error) {
// Image was rejected by moderation or timed out
console.error(error.message);
}
```
Set a prompt alongside the image.
Enhance the prompt automatically.
Timeout in milliseconds.
Reference images must be under 10 MB. The SDK rejects oversized images before sending them.
### `disconnect()`
End the session and clean up all WebRTC and WebSocket connections.
```typescript theme={null}
rt.disconnect();
```
### `isConnected()`
Returns `true` if the SDK has an active connection.
```typescript theme={null}
if (rt.isConnected()) {
await rt.set({ prompt: "New prompt" });
}
```
### `getConnectionState()`
Returns the current connection state.
```typescript theme={null}
const state = rt.getConnectionState();
// "connecting" | "connected" | "generating" | "disconnected" | "reconnecting"
```
### `sessionId`
The current session identifier, or `null` if not connected.
```typescript theme={null}
console.log("Session:", rt.sessionId);
```
### Events
Listen to events with `rt.on(event, listener)` and remove listeners with `rt.off(event, listener)`.
| Event | Payload | Description |
| - | - | - |
| `connectionChange` | `ConnectionState` | Connection state changed — `"connecting"`, `"connected"`, `"generating"`, `"disconnected"`, `"reconnecting"` |
| `generationTick` | `{ seconds: number }` | Total elapsed generation time (cumulative). Use for billing. |
| `error` | `DecartSDKError` | An error occurred. The error has a `message` property with details. |
The SDK automatically handles ICE restarts and WebRTC reconnection. You don't need to manage the WebRTC lifecycle — just listen to `connectionChange` to update your UI.
## Error handling
The SDK surfaces errors through the `error` event and by throwing from async methods like `set()`, `setPrompt()`, and `setImage()`.
```typescript theme={null}
// Global error listener
rt.on("error", (error) => {
console.error("Decart error:", error.message);
if (!rt.isConnected()) {
// Connection lost — show reconnecting UI or clean up
showReconnectingState();
}
});
// Per-call error handling
try {
await rt.setPrompt("Transform into anime character");
} catch (error) {
// Moderation rejection or timeout
showNotification("Prompt not accepted. Try a different description.");
}
```
Common error scenarios:
* **Moderation rejection** — prompt or image violates content policy. Caught by `set()`, `setPrompt()`, and `setImage()` (all throw on rejection).
* **Connection lost** — network issue or server disconnect. The SDK attempts to reconnect automatically. Listen to `connectionChange` for state transitions.
* **Invalid API key** — the session fails to connect. Caught during `connect()`.
* **Insufficient credits** — the session ends with a `connectionChange` to `"disconnected"`.
## Next steps
Set up secure token rotation for frontend auth
Optimize camera setup, prompts, and error handling
# HTTP Signaling
Source: https://docs.platform.decart.ai/integrations/signaling-proxy-http
Use stateless HTTP endpoints for signaling with direct WebRTC media
HTTP signaling is in private preview. [Contact us](mailto:contact@decart.ai) to request access.
Your platform proxies stateless HTTP control endpoints for WebRTC signaling and session control. Media (video and audio) flows directly between the end user and Decart over WebRTC. A long-lived Server-Sent Events (SSE) stream carries server-to-client events.
A single `POST` creates the session, exchanges the SDP offer/answer, and optionally sets the initial prompt and reference image — all in one round-trip.
## When to use this path
* Your platform is HTTP-native (REST API gateway, serverless functions, containers)
* You want white-label endpoints without managing stateful WebSocket connections
* You want to use standard API gateway tooling (rate limiting, auth, logging)
* You prefer stateless infrastructure that scales horizontally
This is the recommended path for most API platforms. If you can proxy HTTP, you can integrate Decart realtime.
## Characteristics
| Property | Value |
| - | - |
| White-label | Yes — end users only see your HTTP URLs |
| Frame access | No — media bypasses your proxy |
| Provider visibility | Full — you see every HTTP request (prompts, images, session events) |
| Client requirements | Browser or native app with WebRTC (no Decart SDK needed) |
| Your infrastructure | Stateless HTTP proxy + one SSE stream per active session |
Media quality is identical to using Decart directly. HTTP signaling adds marginal latency to infrequent control operations — not to the video stream.
## Architecture
```mermaid theme={null}
sequenceDiagram
participant U as End User
participant P as Your HTTP Proxy
participant D as Decart
rect rgb(240, 245, 255)
Note over U,D: Session + SDP Exchange (single round-trip)
U->>P: POST /sessions {model, sdp_offer, prompt?, image?}
P->>D: POST /v1/realtime/sessions
D-->>P: 201 Created {sdp_answer, ice_servers, event_token}
P-->>U: 201 Created {sdp_answer, ice_servers, event_token}
end
rect rgb(245, 240, 255)
Note over U,D: SSE + ICE Trickle
U->>P: GET /sessions/{id}/events?event_token=...
P->>D: GET /v1/realtime/sessions/{id}/events?event_token=...
D-->>P: SSE stream
P-->>U: SSE stream
U->>P: PATCH /sessions/{id} {candidates}
P->>D: PATCH /v1/realtime/sessions/{id}
D-->>P: 204 No Content
P-->>U: 204 No Content
end
rect rgb(240, 255, 240)
Note over U,D: Session Control (stateless HTTP calls)
U->>P: POST /sessions/{id}/prompt
P->>D: POST /v1/realtime/sessions/{id}/prompt
D-->>P: 200 OK
P-->>U: 200 OK
end
Note over U,D: WebRTC media flows directly to Decart Video & audio bypass your proxy entirely
U->>D: WebRTC media (direct)
D->>U: WebRTC media (direct)
```
## How it works
A single `POST` creates the realtime session, exchanges the SDP offer/answer, and returns ICE server configuration. You can optionally include an initial prompt and reference image to pre-configure the generation before the first frame.
Your backend sends the model selection and the client's SDP offer. Decart returns a `201 Created` with the SDP answer, a `Location` header pointing to the session resource, and an `ETag` for ICE session tracking.
```bash cURL theme={null}
curl -X POST https://api.decart.ai/v1/realtime/sessions \
-H "x-api-key: $DECART_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "lucy-2.1",
"sdp": {
"type": "offer",
"sdp": "v=0\r\no=- 123 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0 1\r\n..."
},
"prompt": "Anime style portrait",
"enhance_prompt": true
}'
```
```http Response theme={null}
HTTP/1.1 201 Created
Location: https://api.decart.ai/v1/realtime/sessions/rs_abc123
ETag: "ice-1a2b3c"
Content-Type: application/json
{
"session_id": "rs_abc123",
"sdp": {
"type": "answer",
"sdp": "v=0\r\no=- 456 2 IN IP4 34.102.85.17\r\ns=-\r\nt=0 0\r\n..."
},
"ice_servers": [
{ "urls": "stun:stun.l.google.com:19302" },
{
"urls": "turn:turn.decart.ai:3478",
"username": "...",
"credential": "..."
}
],
"events": {
"url": "https://api.decart.ai/v1/realtime/sessions/rs_abc123/events",
"event_token": "evt_eyJhbGciOi...",
"expires_at": "2025-01-15T12:30:00Z"
},
"expires_at": "2025-01-15T12:30:00Z"
}
```
The `sdp` object matches the browser's `RTCSessionDescription` format. You can pass `pc.localDescription` directly and apply the response with `pc.setRemoteDescription(response.sdp)`.
The optional fields let you front-load session configuration in the same round-trip:
Model identifier (e.g., `lucy-2.1`).
Client's SDP offer — `{ "type": "offer", "sdp": "..." }`.
Set the initial prompt before generation starts.
Enhance the prompt automatically (default: `true`).
Base64-encoded reference image (max 10 MB).
When you include `prompt` or `image_data`, the server validates and moderates the content before creating the session. If moderation rejects it, the request fails with `422` — no session is created and no resources are allocated.
The response includes everything needed to complete the WebRTC connection:
Unique session identifier.
SDP answer — pass directly to `pc.setRemoteDescription(response.sdp)`.
Always `"answer"`.
Full SDP string (same format as `RTCSessionDescription.sdp`).
ICE server configuration — pass to `new RTCPeerConnection({ iceServers: response.ice_servers })`.
SSE connection details for the server event stream.
SSE endpoint URL.
Ephemeral token for SSE auth — safe to pass to the client.
ISO 8601 expiration timestamp.
ISO 8601 session expiration timestamp.
Right after session creation, forward ICE candidates from the client using `PATCH` with the `If-Match` header. The client opens the SSE stream using the `event_token` from the session response.
The candidate format matches `RTCIceCandidate.toJSON()` — forward them directly from the browser. Signal end-of-candidates by sending a `null` candidate.
```bash ICE Candidate theme={null}
curl -X PATCH https://api.decart.ai/v1/realtime/sessions/rs_abc123 \
-H "x-api-key: $DECART_API_KEY" \
-H "Content-Type: application/json" \
-H 'If-Match: "ice-1a2b3c"' \
-d '{
"candidates": [
{
"candidate": "candidate:1 1 UDP 2130706431 192.168.1.5 54321 typ host",
"sdpMLineIndex": 0,
"sdpMid": "0"
}
]
}'
```
```bash End of Candidates theme={null}
curl -X PATCH https://api.decart.ai/v1/realtime/sessions/rs_abc123 \
-H "x-api-key: $DECART_API_KEY" \
-H "Content-Type: application/json" \
-H 'If-Match: "ice-2b3c4d"' \
-d '{
"candidates": [null]
}'
```
```http Response theme={null}
HTTP/1.1 204 No Content
ETag: "ice-2b3c4d"
```
Server-side ICE candidates are delivered via the [SSE event stream](#handle-server-events-sse). The server sends `null` as the last candidate to signal end-of-candidates from its side.
**ETag is required on every `PATCH`.** If you omit `If-Match`, the server responds with `428 Precondition Required`. If the ETag doesn't match (another ICE operation completed first), you get `412 Precondition Failed` — call `GET /v1/realtime/sessions/{id}` to fetch the current ETag and retry. The server queues concurrent ICE candidates correctly, so rapid-fire PATCHes work as long as each carries a valid ETag.
The client opens an SSE connection using the `event_token` from the session creation response. Handle generation lifecycle, server-side ICE candidates, moderation alerts, and errors.
```typescript theme={null}
const events = new EventSource(
`https://api.yourplatform.com/v1/realtime/sessions/${sessionId}/events?event_token=${encodeURIComponent(eventToken)}`
);
events.addEventListener("generation_started", () => {
showLiveIndicator();
});
events.addEventListener("generation_tick", (e) => {
const { seconds } = JSON.parse(e.data);
updateUsageDisplay(seconds);
});
events.addEventListener("ice-candidate", (e) => {
const data = JSON.parse(e.data);
if (data.candidate === null) {
// Server signals end-of-candidates
peerConnection.addIceCandidate(null);
} else {
const { candidate, sdpMLineIndex, sdpMid } = data;
peerConnection.addIceCandidate({ candidate, sdpMLineIndex, sdpMid });
}
});
events.addEventListener("ice-restart", async (e) => {
const { ice_servers } = JSON.parse(e.data);
// Server requests ICE restart — reconfigure with new TURN credentials
peerConnection.setConfiguration({ iceServers: ice_servers });
const offer = await peerConnection.createOffer({ iceRestart: true });
await peerConnection.setLocalDescription(offer);
// Send PATCH with If-Match: "*" and the new SDP offer
});
events.addEventListener("generation_ended", (e) => {
const { seconds, reason } = JSON.parse(e.data);
finalizeSession(seconds, reason);
// SSE stream closes after this event
});
events.addEventListener("error", (e) => {
const error = JSON.parse(e.data);
handleError(error.title, error.detail);
});
```
The SSE stream is your only channel for server-initiated messages. Keep it open for the entire session. When the session ends, the server sends `generation_ended` followed by a final empty `data: ` field, then closes the stream. If the connection drops mid-session, reconnect — `EventSource` handles this automatically with the `Last-Event-ID` header. If you reconnect after the session has already ended, the server responds with `410 Gone`.
Control the generation with stateless HTTP calls — no persistent connection needed.
```bash Prompt theme={null}
curl -X POST https://api.decart.ai/v1/realtime/sessions/rs_abc123/prompt \
-H "x-api-key: $DECART_API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt": "Anime style", "enhance_prompt": true}'
```
```json Response theme={null}
{
"prompt": "Anime style"
}
```
```bash Reference Image theme={null}
curl -X POST https://api.decart.ai/v1/realtime/sessions/rs_abc123/image \
-H "x-api-key: $DECART_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"image_data": "",
"prompt": "Transform into this character",
"enhance_prompt": true
}'
```
```http Response theme={null}
HTTP/1.1 200 OK
Content-Type: application/json
{}
```
The prompt response echoes back the original prompt text. A `200` status code confirms the content was accepted — no additional `success` field is needed. Both endpoints moderate input content synchronously — if the content is rejected, the API returns `422 Unprocessable Entity`:
```json theme={null}
{
"type": "https://api.decart.ai/errors/moderation-rejected",
"title": "Input rejected by moderation",
"detail": "Content violates our Terms of Service",
"status": 422
}
```
Reference images must be under 10 MB (base64-encoded). Requests with oversized images are rejected with `413 Content Too Large`.
If the network path degrades, the server sends an `ice-restart` SSE event with updated TURN credentials. Respond by creating a new SDP offer (with the provided ICE servers) and sending it as a `PATCH` with `If-Match: "*"` (wildcard).
```bash cURL theme={null}
curl -X PATCH https://api.decart.ai/v1/realtime/sessions/rs_abc123 \
-H "x-api-key: $DECART_API_KEY" \
-H "Content-Type: application/json" \
-H 'If-Match: "*"' \
-d '{
"sdp": {
"type": "offer",
"sdp": "v=0\r\no=- 123 3 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\n..."
}
}'
```
```http Response theme={null}
HTTP/1.1 200 OK
ETag: "ice-4d5e6f"
Content-Type: application/json
{
"sdp": {
"type": "answer",
"sdp": "v=0\r\no=- 456 3 IN IP4 34.102.85.17\r\ns=-\r\nt=0 0\r\n..."
}
}
```
The response includes a new `ETag` — use it for any subsequent ICE operations.
ICE restarts are server-initiated. The server makes one restart attempt with TURN credentials — if it fails, the session ends with a `generation_ended` event (reason: `"ice_failure"`).
Explicitly close the session when the user disconnects. The response includes a session summary with billing information.
```bash cURL theme={null}
curl -X DELETE https://api.decart.ai/v1/realtime/sessions/rs_abc123 \
-H "x-api-key: $DECART_API_KEY"
```
```json Response theme={null}
{
"session_id": "rs_abc123",
"duration_seconds": 120,
"billed_seconds": 120,
"reason": "disconnect"
}
```
Sessions also end automatically when:
* The WebRTC connection fails and ICE restart does not recover it
* A moderation violation terminates the stream
* The session reaches the maximum duration
* No media activity is detected (inactivity timeout)
In all cases, a `generation_ended` SSE event is sent before the stream closes.
## API reference
### Endpoints
| Method | Path | Description |
| - | - | - |
| `POST` | `/v1/realtime/sessions` | Create session + exchange SDP offer/answer |
| `PATCH` | `/v1/realtime/sessions/{id}` | Trickle ICE candidates or trigger ICE restart |
| `GET` | `/v1/realtime/sessions/{id}` | Read session state, current ETag, and refresh `event_token` |
| `GET` | `/v1/realtime/sessions/{id}/events` | SSE stream for server events |
| `POST` | `/v1/realtime/sessions/{id}/prompt` | Set or update the model prompt |
| `POST` | `/v1/realtime/sessions/{id}/image` | Set reference image (with optional prompt) |
| `DELETE` | `/v1/realtime/sessions/{id}` | End the session and return billing summary |
### Session state
Use `GET /v1/realtime/sessions/{id}` to recover state after reconnects, to fetch the current ETag before retrying a `412` ICE operation, or to obtain a fresh `event_token` if the SSE connection dropped.
```bash cURL theme={null}
curl https://api.decart.ai/v1/realtime/sessions/rs_abc123 \
-H "x-api-key: $DECART_API_KEY"
```
```json Response theme={null}
{
"session_id": "rs_abc123",
"state": "active",
"etag": "ice-2b3c4d",
"events": {
"url": "https://api.decart.ai/v1/realtime/sessions/rs_abc123/events",
"event_token": "evt_freshTokenHere...",
"expires_at": "2025-01-15T12:30:00Z"
},
"expires_at": "2025-01-15T12:30:00Z",
"started_at": "2025-01-15T12:00:00Z",
"generation_seconds": 36,
"billed_seconds": 36
}
```
Each `GET` returns a fresh `event_token` that expires at the same time as the session. This avoids the need for a separate token refresh endpoint.
### Authentication
All control endpoints require an `x-api-key` header with a valid Decart API key. The SSE endpoint uses a short-lived `event_token` returned by `POST /v1/realtime/sessions` (and refreshable via `GET /v1/realtime/sessions/{id}`):
```
GET /v1/realtime/sessions/{id}/events?event_token=EVT_TOKEN
```
The `event_token` is scoped to one session and expires when the session does. This avoids placing your long-lived API key in browser-visible URLs while keeping `x-api-key` as the upstream authentication method for all other endpoints. The token is ephemeral, read-only (SSE is server-to-client), and safe to pass to the client.
### SSE event types
| Event | Data | Description |
| - | - | - |
| `ice-candidate` | `{"candidate": "...", "sdpMLineIndex": 0, "sdpMid": "0"}` | Server-side ICE candidate — forward to `RTCPeerConnection`. A `null` candidate signals end-of-candidates. |
| `ice-restart` | `{"ice_servers": [{"urls": "turn:...", "username": "...", "credential": "..."}]}` | Server requests ICE restart — reconfigure and send new offer via `PATCH` |
| `generation_started` | `{}` | Model is producing frames — media will start flowing |
| `generation_tick` | `{"seconds": 30}` | Total elapsed generation time in seconds (cumulative, not a delta). Use for usage tracking. |
| `generation_ended` | `{"seconds": 120, "reason": "disconnect"}` | Session complete. SSE stream closes after this event. |
| `error` | `{"type": "...", "title": "...", "detail": "...", "status": 500}` | Server error |
The `generation_ended` reason field indicates why the session ended:
| Reason | Description |
| - | - |
| `disconnect` | Client disconnected |
| `timeout` | Session reached the maximum duration |
| `moderation_violation` | Content policy violation terminated the stream |
| `error` | Server error |
| `insufficient_credits` | Account has insufficient credits |
### Error responses
All errors return a structured JSON body:
```json theme={null}
{
"type": "https://api.decart.ai/errors/invalid-sdp",
"title": "Invalid SDP offer",
"detail": "Missing ice-ufrag attribute in the SDP offer",
"status": 400
}
```
| Status | When |
| - | - |
| `400` | Malformed request body or invalid SDP |
| `401` | Missing or invalid API key |
| `404` | Session not found or expired |
| `410` | Session ended — SSE reconnect after termination |
| `412` | `If-Match` ETag doesn't match current ICE session state |
| `413` | Image exceeds the 10 MB size limit |
| `422` | Valid JSON but semantically invalid (unsupported model, content rejected by moderation) |
| `428` | `PATCH` sent without required `If-Match` header |
| `503` | Service temporarily unavailable — check the `Retry-After` header |
## Provider proxy example
Your control endpoints stay stateless — every request is an independent pass-through, including SSE. The client provides the `event_token` (received during session creation) to connect to the SSE stream through your proxy.
```typescript theme={null}
// Next.js / Express — all routes are stateless pass-through
app.all("/v1/realtime/*", async (req, res) => {
const decartUrl = `https://api.decart.ai${req.path}${req.url.includes("?") ? req.url.substring(req.url.indexOf("?")) : ""}`;
// Build headers — inject API key, forward If-Match for ICE operations
const headers: Record = {
"x-api-key": process.env.DECART_API_KEY!,
};
if (req.headers["content-type"]) {
headers["Content-Type"] = req.headers["content-type"] as string;
}
if (req.headers["if-match"]) {
headers["If-Match"] = req.headers["if-match"] as string;
}
const response = await fetch(decartUrl, {
method: req.method,
headers,
body: ["GET", "DELETE"].includes(req.method)
? undefined
: JSON.stringify(req.body),
});
// Forward response headers
for (const header of ["location", "etag"]) {
const value = response.headers.get(header);
if (value) res.setHeader(header, value);
}
// SSE — pipe the stream directly to the client
if (req.path.endsWith("/events")) {
res.status(response.status);
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
response.body?.pipeTo(
new WritableStream({
write(chunk) { res.write(chunk); },
close() { res.end(); },
})
);
return;
}
if (response.status === 204) {
return res.status(204).end();
}
const data = await response.json();
res.status(response.status).json(data);
});
```
Your proxy must forward the `ETag` and `Location` headers from Decart's response, and pass through `If-Match` from client requests. The `event_token` is ephemeral, session-scoped, and read-only — safe to pass to the client. You can still add your own authentication, rate limiting, and logging on top.
## Client implementation
Your client talks to your HTTP proxy for signaling and handles WebRTC with standard browser APIs.
```javascript theme={null}
const pc = new RTCPeerConnection({
iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
});
pc.ontrack = (event) => {
document.getElementById("remote-video").srcObject = event.streams[0];
};
const stream = await navigator.mediaDevices.getUserMedia({
video: { width: 1280, height: 720, frameRate: 20 },
audio: true,
});
stream.getTracks().forEach((track) => pc.addTrack(track, stream));
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// Create session — SDP exchange + ICE servers in one round-trip
const BASE = "https://api.yourplatform.com/v1/realtime";
const createRes = await fetch(`${BASE}/sessions`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "lucy-2.1",
sdp: { type: "offer", sdp: offer.sdp },
prompt: "Anime style",
enhance_prompt: true,
}),
});
const session = await createRes.json();
const sessionId = session.session_id;
let etag = createRes.headers.get("ETag");
await pc.setRemoteDescription(session.sdp);
pc.setConfiguration({ iceServers: session.ice_servers });
// Queue candidates and flush serially to maintain ETag ordering
const candidateQueue = [];
let flushing = false;
async function flushCandidates() {
if (flushing || candidateQueue.length === 0) return;
flushing = true;
const batch = candidateQueue.splice(0);
const res = await fetch(`${BASE}/sessions/${sessionId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json", "If-Match": etag },
body: JSON.stringify({ candidates: batch }),
});
etag = res.headers.get("ETag") || etag;
flushing = false;
if (candidateQueue.length > 0) flushCandidates();
}
pc.onicecandidate = ({ candidate }) => {
candidateQueue.push(candidate ? candidate.toJSON() : null);
flushCandidates();
};
// SSE for server-initiated events
const sse = new EventSource(
`${BASE}/sessions/${sessionId}/events?event_token=${encodeURIComponent(session.events.event_token)}`
);
sse.addEventListener("ice-candidate", async (e) => {
const data = JSON.parse(e.data);
await pc.addIceCandidate(data.candidate === null ? null : data);
});
sse.addEventListener("ice-restart", async (e) => {
const { ice_servers } = JSON.parse(e.data);
pc.setConfiguration({ iceServers: ice_servers });
const restartOffer = await pc.createOffer({ iceRestart: true });
await pc.setLocalDescription(restartOffer);
// If-Match: "*" bypasses ETag sequencing during restarts
const res = await fetch(`${BASE}/sessions/${sessionId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json", "If-Match": "*" },
body: JSON.stringify({ sdp: { type: "offer", sdp: restartOffer.sdp } }),
});
etag = res.headers.get("ETag") || etag;
await pc.setRemoteDescription((await res.json()).sdp);
});
sse.addEventListener("generation_started", () => {
document.getElementById("status").textContent = "● Live";
});
sse.addEventListener("generation_tick", (e) => {
document.getElementById("usage").textContent = `${JSON.parse(e.data).seconds}s`;
});
sse.addEventListener("generation_ended", () => {
document.getElementById("status").textContent = "Ended";
sse.close();
});
// Session controls
window.setPrompt = (text) =>
fetch(`${BASE}/sessions/${sessionId}/prompt`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt: text }),
});
window.disconnect = async () => {
await fetch(`${BASE}/sessions/${sessionId}`, { method: "DELETE" });
stream.getTracks().forEach((t) => t.stop());
sse.close();
pc.close();
};
```
The queue-and-flush pattern batches ICE candidates into fewer `PATCH` requests while maintaining ETag ordering. In production, retry `412` responses by fetching the current ETag with `GET /sessions/{id}`.
## Next steps
Alternative: proxy via WebSocket instead of HTTP
API key management and client tokens
# WS Signaling Proxy
Source: https://docs.platform.decart.ai/integrations/signaling-proxy-ws
Proxy WebSocket signaling for a white-labeled integration while keeping WebRTC direct
Your platform runs a WebSocket proxy that sits between the end user and Decart for the control plane (signaling, prompts, session events). Media (video and audio) flows directly between the end user and Decart over WebRTC — your proxy never touches it.
This gives you a white-labeled experience with full media quality and visibility into all control messages.
## When to use this path
* Your platform already runs WebSocket infrastructure
* You want full visibility into the control plane (prompts, images, session events)
* You want white-labeled endpoints — end users never see Decart
If your platform is HTTP-native and you'd prefer not to run stateful WebSocket connections, see [HTTP Signaling](/integrations/signaling-proxy-http) — same quality, same white-label, but with stateless HTTP proxy instead.
## Characteristics
| Property | Value |
| - | - |
| White-label | Yes — end users only see your WS URL |
| Frame access | No — media bypasses your proxy |
| Provider visibility | Full — you see every signaling message, prompt, and image |
| Client requirements | Browser or native app with WebRTC (no Decart SDK needed) |
| Your infrastructure | WebSocket proxy server |
Media quality is identical to using Decart directly. Your WS proxy only handles signaling — video and audio flow peer-to-peer.
## Reference implementation
A complete working proxy in \~200 lines of TypeScript — message buffering, close-code sanitization, structured logging, and an e2e test.
## Architecture
```mermaid theme={null}
sequenceDiagram
participant U as End User
participant P as Your WS Proxy
participant D as Decart
U->>P: WS connect
P->>P: Authenticate user
P->>D: WS connect (your API key)
rect rgb(240, 245, 255)
Note over U,D: WebRTC Signaling (proxied)
U->>P: offer SDP
P->>D: forward
D-->>P: answer SDP
P-->>U: answer SDP
U->>P: ICE candidates
P->>D: forward
D-->>P: ICE candidates
P-->>U: ICE candidates
end
rect rgb(240, 255, 240)
Note over U,D: Control Messages (proxied)
U->>P: prompt
P->>P: Log / modify
P->>D: forward
D-->>P: prompt_ack
P-->>U: prompt_ack
D-->>P: generation_tick
P-->>U: generation_tick
D-->>P: generation_ended
P-->>U: generation_ended
end
Note over U,D: WebRTC media flows directly (P2P) Video & audio bypass your proxy entirely
U->>D: WebRTC media (direct via IP:port)
D->>U: WebRTC media (direct via IP:port)
```
## How it works
The end user connects to your platform's WS URL — they never see a Decart endpoint.
```
wss://api.yourplatform.com/v1/realtime?model=lucy-2.1&token=user_token
```
Authenticate with Decart using your platform API key and forward the model selection.
```python theme={null}
import websockets
upstream = await websockets.connect(
f"wss://api.decart.ai/v1/stream?api_key={DECART_API_KEY}&model={model}"
)
```
Your proxy connects to Decart with **your** API key.
Forward connect-time query parameters your end users rely on — in particular `speed`. [Fast mode](/models/realtime/lucy-2.5#fast-mode) (`speed=fast`, billed at 2x) is selected on this URL; a proxy that rebuilds it with only `model` silently lands every session on the standard tier. Omit the parameter entirely for standard mode.
Pump JSON messages between the client and Decart. Every message has a `type` field. You can log, validate, or modify messages as they pass through — see the [message reference](#message-reference) for the full protocol.
```python theme={null}
import asyncio
import json
async def client_to_upstream(client_ws, upstream_ws):
async for raw_msg in client_ws:
msg = json.loads(raw_msg)
if msg["type"] == "prompt":
log_prompt(msg["prompt"])
# Optional: validate or modify before forwarding
elif msg["type"] == "set_image":
log_image_upload()
# Optional: validate image size / content
await upstream_ws.send(raw_msg)
async def upstream_to_client(upstream_ws, client_ws):
async for raw_msg in upstream_ws:
msg = json.loads(raw_msg)
if msg["type"] == "session_id":
log_session(msg["session_id"])
elif msg["type"] == "prompt_ack" and not msg["success"]:
log_moderation_rejection(msg["error"])
elif msg["type"] == "generation_ended":
log_session_end(msg["seconds"], msg["reason"])
await client_ws.send(raw_msg)
await asyncio.gather(
client_to_upstream(client_ws, upstream),
upstream_to_client(upstream, client_ws),
)
```
When the end user's browser receives the SDP answer and ICE candidates through your proxy, it establishes a direct WebRTC connection to Decart via the IP:port in the ICE candidates. Your proxy is not involved in media.
When either side disconnects, close the other connection. Your proxy should handle both directions:
```python theme={null}
async def proxy_session(client_ws, model):
upstream = await websockets.connect(
f"wss://api.decart.ai/v1/stream?api_key={DECART_API_KEY}&model={model}"
)
try:
await asyncio.gather(
client_to_upstream(client_ws, upstream),
upstream_to_client(upstream, client_ws),
)
except websockets.ConnectionClosed:
pass
finally:
await upstream.close()
```
## Message reference
Your proxy forwards JSON messages between the client and Decart. Every message has a `type` field that identifies it.
Messages your proxy receives from the client and forwards to Decart.
```json theme={null}
{ "type": "offer", "sdp": "v=0\r\no=- 123 2 IN IP4 127.0.0.1\r\n..." }
```
The `sdp` field is a string containing the full SDP (same format as `RTCSessionDescription.sdp`). Forward as-is.
```json theme={null}
{
"type": "ice-candidate",
"candidate": {
"candidate": "candidate:1 1 UDP 2130706431 192.168.1.5 54321 typ host",
"sdpMLineIndex": 0,
"sdpMid": "0",
"usernameFragment": "ab12"
}
}
```
Forward as-is. The candidate format matches `RTCIceCandidate.toJSON()` — `usernameFragment` is optional and may be present depending on the browser. Signal end-of-candidates by sending `null`:
```json theme={null}
{ "type": "ice-candidate", "candidate": null }
```
```json theme={null}
{ "type": "prompt", "prompt": "Anime style portrait", "enhance_prompt": true }
```
| Field | Type | Description |
| - | - | - |
| `prompt` | `string` | The text prompt to apply |
| `enhance_prompt` | `boolean` | When `true` (default), the server enhances the prompt automatically before applying it. Set to `false` to use the exact prompt text. |
You **can** validate, filter, or replace this message before forwarding.
```json theme={null}
{
"type": "set_image",
"image_data": "",
"prompt": "Transform into this character",
"enhance_prompt": true
}
```
| Field | Type | Description |
| - | - | - |
| `image_data` | `string \| null` | Base64-encoded image (max 10 MB decoded). Send `null` to clear the current reference image. |
| `prompt` | `string` | Optional. Set a prompt alongside the image. |
| `enhance_prompt` | `boolean` | Optional. Enhance the prompt automatically (default: `true`). |
You **can** validate or reject this message before forwarding.
Reference images must be under 10 MB (decoded). Oversized images are rejected by the server.
Messages your proxy receives from Decart and forwards to the client.
```json theme={null}
{ "type": "answer", "sdp": "v=0\r\no=- 456 2 IN IP4 34.102.85.17\r\n..." }
```
Forward as-is. The client applies this with `pc.setRemoteDescription({ type: "answer", sdp: msg.sdp })`.
```json theme={null}
{
"type": "ice-candidate",
"candidate": {
"candidate": "candidate:1 1 UDP 2130706431 34.102.85.17 12345 typ host",
"sdpMLineIndex": 0,
"sdpMid": "0",
"usernameFragment": "ab12"
}
}
```
Forward as-is. A `null` candidate signals end-of-candidates from the server side.
ICE candidates are high-frequency during connection setup. Avoid logging individual candidates — log connection state changes instead.
```json theme={null}
{
"type": "session_id",
"session_id": "abc123",
"server_ip": "34.102.85.17",
"server_port": 12345
}
```
Log the `session_id` for tracking and support. The `server_ip` and `server_port` indicate which Decart server is handling this session.
```json theme={null}
{ "type": "prompt_ack", "prompt": "Anime style portrait", "success": true, "error": null }
```
When moderation rejects a prompt:
```json theme={null}
{ "type": "prompt_ack", "prompt": "...", "success": false, "error": "Content violates our Terms of Service" }
```
Forward to the client so they know whether their prompt was applied. The `prompt` field echoes back the original text.
```json theme={null}
{ "type": "set_image_ack", "success": true, "error": null }
```
When moderation rejects an image:
```json theme={null}
{ "type": "set_image_ack", "success": false, "error": "Content violates our Terms of Service" }
```
Forward to the client so they know whether their image was applied.
```json theme={null}
{ "type": "generation_started" }
```
Media will start flowing over the WebRTC connection after this message.
```json theme={null}
{ "type": "generation_tick", "seconds": 30 }
```
The `seconds` field is the total elapsed generation time (cumulative, not a delta). Use this for usage tracking and billing.
```json theme={null}
{ "type": "generation_ended", "seconds": 120, "reason": "disconnect" }
```
The WebSocket connection closes shortly after this message. The `reason` field indicates why the session ended:
| Reason | Description |
| - | - |
| `disconnect` | Client disconnected |
| `timeout` | Session reached the maximum duration |
| `moderation_violation` | Content policy violation terminated the stream |
| `error` | Server error |
| `insufficient_credits` | Account has insufficient credits |
```json theme={null}
{ "type": "error", "error": "Connection failed" }
```
The `error` field is a human-readable error string. Forward to the client or handle in your proxy.
```json theme={null}
{
"type": "ice-restart",
"turn_config": {
"username": "...",
"credential": "...",
"server_url": "turn:turn.decart.ai:3478"
}
}
```
When present, `turn_config` provides fresh TURN credentials. Forward to the client — they should reconfigure their peer connection with the new TURN server and create a new SDP offer.
ICE restarts are server-initiated. If the restart fails, the session ends with a `generation_ended` message (reason: `"error"`).
## Moderation
Prompts and images are moderated server-side before being applied to the model. Your proxy sees the result as acknowledgment messages:
* **Prompt accepted** → `prompt_ack` with `success: true`
* **Prompt rejected** → `prompt_ack` with `success: false` and an `error` string
* **Image accepted** → `set_image_ack` with `success: true`
* **Image rejected** → `set_image_ack` with `success: false` and an `error` string
Rejected content is not applied — the model continues with the previous prompt or image. Your proxy can add its own moderation layer before forwarding messages to Decart.
## Usage tracking
Track usage by watching the generation lifecycle messages:
```python theme={null}
async def upstream_to_client(upstream_ws, client_ws, session):
async for raw_msg in upstream_ws:
msg = json.loads(raw_msg)
if msg["type"] == "generation_started":
session.mark_started()
elif msg["type"] == "generation_tick":
session.record_usage(seconds=msg["seconds"])
elif msg["type"] == "generation_ended":
session.finalize(
total_seconds=msg["seconds"],
reason=msg["reason"],
)
await client_ws.send(raw_msg)
```
## Proxy example
A complete WebSocket proxy using Python and `websockets`. This example authenticates the client, connects upstream, pumps messages with logging, and handles disconnects:
```python theme={null}
import asyncio
import json
import websockets
DECART_API_KEY = "your-api-key" # Load from environment in production
async def handle_client(client_ws):
# 1. Authenticate the client and extract model (and optional speed) from query params
model = parse_model_from_query(client_ws.request)
speed = parse_speed_from_query(client_ws.request) # "fast" for fast mode, else None
user = authenticate_user(client_ws.request)
if not user:
await client_ws.close(4001, "Unauthorized")
return
# 2. Open upstream connection to Decart — forward `speed` only when set
upstream_url = f"wss://api.decart.ai/v1/stream?api_key={DECART_API_KEY}&model={model}"
if speed:
upstream_url += f"&speed={speed}"
async with websockets.connect(upstream_url) as upstream_ws:
session_id = None
async def client_to_upstream():
async for raw_msg in client_ws:
msg = json.loads(raw_msg)
if msg["type"] == "prompt":
print(f"[{session_id}] prompt: {msg['prompt'][:80]}")
elif msg["type"] == "set_image":
print(f"[{session_id}] set_image (has_prompt={bool(msg.get('prompt'))})")
await upstream_ws.send(raw_msg)
async def upstream_to_client():
nonlocal session_id
async for raw_msg in upstream_ws:
msg = json.loads(raw_msg)
if msg["type"] == "session_id":
session_id = msg["session_id"]
print(f"Session started: {session_id}")
elif msg["type"] == "prompt_ack":
if not msg["success"]:
print(f"[{session_id}] prompt rejected: {msg['error']}")
elif msg["type"] == "set_image_ack":
if not msg["success"]:
print(f"[{session_id}] image rejected: {msg['error']}")
elif msg["type"] == "generation_started":
print(f"[{session_id}] generation started")
elif msg["type"] == "generation_ended":
print(f"[{session_id}] ended: {msg['reason']} ({msg['seconds']}s)")
record_billing(user, session_id, msg["seconds"])
elif msg["type"] == "error":
print(f"[{session_id}] error: {msg['error']}")
await client_ws.send(raw_msg)
# 3. Pump messages in both directions
try:
await asyncio.gather(
client_to_upstream(),
upstream_to_client(),
)
except websockets.ConnectionClosed:
pass
print(f"[{session_id}] connection closed")
async def main():
async with websockets.serve(handle_client, "0.0.0.0", 8080):
await asyncio.Future() # Run forever
asyncio.run(main())
```
This proxy is stateless — it holds no session state between connections. Each WebSocket connection is an independent pass-through. You can scale horizontally by running multiple proxy instances behind a load balancer.
## Client implementation
Your client connects to the proxy over WebSocket and handles WebRTC with standard browser APIs.
```javascript theme={null}
const ws = new WebSocket(
"wss://api.yourplatform.com/v1/realtime?model=lucy-2.1&token=user_token"
);
const pc = new RTCPeerConnection({
iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
});
const stream = await navigator.mediaDevices.getUserMedia({
video: { width: 1280, height: 720, frameRate: 20 },
audio: true,
});
stream.getTracks().forEach((track) => pc.addTrack(track, stream));
pc.ontrack = (event) => {
document.getElementById("remote-video").srcObject = event.streams[0];
};
pc.onicecandidate = ({ candidate }) => {
ws.send(JSON.stringify({ type: "ice-candidate", candidate }));
};
ws.addEventListener("open", async () => {
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
ws.send(JSON.stringify({ type: "offer", sdp: offer.sdp }));
});
ws.addEventListener("message", async (event) => {
const msg = JSON.parse(event.data);
switch (msg.type) {
case "answer":
await pc.setRemoteDescription({ type: "answer", sdp: msg.sdp });
break;
case "ice-candidate":
if (msg.candidate) await pc.addIceCandidate(msg.candidate);
break;
case "ice-restart":
// Server rotated TURN credentials — reconfigure and restart ICE
pc.setConfiguration({
iceServers: [
{ urls: "stun:stun.l.google.com:19302" },
{
urls: msg.turn_config.server_url,
username: msg.turn_config.username,
credential: msg.turn_config.credential,
},
],
});
const restartOffer = await pc.createOffer({ iceRestart: true });
await pc.setLocalDescription(restartOffer);
ws.send(JSON.stringify({ type: "offer", sdp: restartOffer.sdp }));
break;
case "generation_started":
showLiveIndicator();
break;
case "generation_tick":
updateUsageDisplay(msg.seconds);
break;
case "generation_ended":
finalizeSession(msg.seconds, msg.reason);
break;
case "prompt_ack":
if (!msg.success) console.warn("Prompt rejected:", msg.error);
break;
case "set_image_ack":
if (!msg.success) console.warn("Image rejected:", msg.error);
break;
case "error":
console.error("Server error:", msg.error);
break;
}
});
```
Once the session is running, send control messages at any time:
```javascript theme={null}
ws.send(JSON.stringify({
type: "prompt",
prompt: "Cyberpunk neon city",
enhance_prompt: true,
}));
stream.getTracks().forEach((t) => t.stop());
ws.close();
pc.close();
```
Message types and fields match the [message reference](#message-reference) above. In production, add reconnection logic for WebSocket drops and monitor `pc.connectionState` for WebRTC failures.
## Next steps
Same architecture with stateless HTTP instead of WS proxy
API key management and client tokens
# Image Editing
Source: https://docs.platform.decart.ai/models/image/image-editing
Learn how to edit and transform existing images with AI.
With the Decart API, you can transform and edit existing images using text prompts. Simply upload an image and describe the changes you want to make, and Lucy Image will intelligently modify your image while preserving important details.
Lucy Image excels at style transfers, object modifications, background replacements, and artistic transformations.
Our models maintain the core structure of your original image while applying your desired edits with high precision.
## Quick start
Here's a simple example using **Lucy Image**, our advanced image editing model.
```bash Curl theme={null}
curl -X POST https://api.decart.ai/v1/generate/lucy-image-2 \
-H "X-API-KEY: $DECART_API_KEY" \
-F "data=@input-image.jpg" \
-F "prompt=Change the sky to sunset colors" \
-F "resolution=720p" \
--output edited-image.jpg
```
```python Python SDK theme={null}
from decart_sdk import create_decart_client, models
client = create_decart_client(api_key="your-api-key-here")
with open("input-image.jpg", "rb") as image_file:
result = await client.process({
"model": models.image("lucy-image-2"),
"prompt": "Change the sky to sunset colors",
"resolution": "720p",
"data": image_file
})
with open("edited-image.jpg", "wb") as f:
f.write(result)
```
```javascript JavaScript SDK theme={null}
import { createDecartClient, models } from "@decartai/sdk";
const client = createDecartClient({ apiKey: "your-api-key-here" });
const result = await client.process({
model: models.image("lucy-image-2"),
prompt: "Change the sky to sunset colors",
resolution: "720p",
data: imageFile
});
const img = document.querySelector("img");
img.src = URL.createObjectURL(result);
```
```python Python (REST) theme={null}
import requests
import os
with open("input-image.jpg", "rb") as image:
response = requests.post(
"https://api.decart.ai/v1/generate/lucy-image-2",
headers={"X-API-KEY": os.getenv("DECART_API_KEY")},
files={"data": image},
data={
"prompt": "Change the sky to sunset colors",
"resolution": "720p"
}
)
with open("edited-image.jpg", "wb") as f:
f.write(response.content)
```
```javascript JavaScript (REST) theme={null}
const formData = new FormData();
formData.append("data", imageFile);
formData.append("prompt", "Change the sky to sunset colors");
formData.append("resolution", "720p");
const response = await fetch("https://api.decart.ai/v1/generate/lucy-image-2", {
method: "POST",
headers: {
"X-API-KEY": process.env.DECART_API_KEY,
},
body: formData,
});
const editedImage = await response.blob();
```
### Parameters
* `data` (**required**) — Input image file to edit.
* `prompt` (**required**) — Text description of the changes to apply.
* `reference_image` (**optional**) — A reference image to guide the edit. Use this to show the model exactly what you want to add or change (e.g., a specific pair of sunglasses, a logo, or a style reference).
* `resolution` (**optional**) — Output size: `480p` or `720p` (default: `720p`).
For complete API documentation including response formats and error codes, see the [API Reference](/api-reference/lucy-image-2).
## Available Models and Options
Decart provides different editing models to balance **precision, creativity, and cost** depending on your use case.
We currently offer:
* **Lucy Image** — our flagship editing model for precise, high-quality image transformations.
Supported resolutions:
* **720p** (720×1280) — detailed, higher quality, ideal for professional edits.
* **480p** (480×832) — faster, cheaper, well-suited for previews or smaller outputs.
## Using Reference Images
For more precise edits, **Lucy Image** supports an optional `reference_image` parameter. This allows you to provide a visual reference that guides the edit—perfect for when you want to add a specific item (like a particular style of sunglasses) or match a specific look.
```bash Curl theme={null}
curl -X POST https://api.decart.ai/v1/generate/lucy-image-2 \
-H "X-API-KEY: $DECART_API_KEY" \
-F "data=@input-image.jpg" \
-F "prompt=Add these sunglasses to the person's face" \
-F "reference_image=@sunglasses.jpg" \
-F "resolution=720p" \
--output edited-image.jpg
```
```python Python SDK theme={null}
from decart_sdk import create_decart_client, models
client = create_decart_client(api_key="your-api-key-here")
with open("input-image.jpg", "rb") as image_file:
with open("sunglasses.jpg", "rb") as reference_image:
result = await client.process({
"model": models.image("lucy-image-2"),
"prompt": "Add these sunglasses to the person's face",
"data": image_file,
"reference_image": reference_image,
"resolution": "720p"
})
with open("edited-image.jpg", "wb") as f:
f.write(result)
```
```javascript JavaScript SDK theme={null}
import { createDecartClient, models } from "@decartai/sdk";
import { readFileSync, writeFileSync } from "fs";
const client = createDecartClient({ apiKey: "your-api-key-here" });
const imageBuffer = readFileSync("input-image.jpg");
const imageBlob = new Blob([imageBuffer], { type: "image/jpeg" });
const referenceBuffer = readFileSync("sunglasses.jpg");
const referenceBlob = new Blob([referenceBuffer], { type: "image/jpeg" });
const result = await client.process({
model: models.image("lucy-image-2"),
prompt: "Add these sunglasses to the person's face",
data: imageBlob,
reference_image: referenceBlob,
resolution: "720p",
});
const buffer = Buffer.from(await result.arrayBuffer());
writeFileSync("edited-image.jpg", buffer);
```
## Prompt Engineering for Edits
Image editing requires different prompt strategies than generation. Your prompts should clearly describe what to change while implicitly indicating what to preserve. Lucy Image interprets your instructions in the context of the existing image.
Effective editing prompts are **specific about changes** (what to modify), **clear about scope** (local vs global edits), and **descriptive about the target result**. Unlike generation, you don't need to describe the entire scene—focus only on what needs to change. Lucy Image excels at identity conservation, edit precision, realism, and prompt adherence.
**Pro tip:** Longer, detailed prompts (20–30 words) describing style, appearance, and context produce significantly better results than short instructions.
## Supported Edit Types
### ✅ Best Performance
#### 1. Clothing Changes
Lucy Image excels at swapping outfits while preserving motion, pose, and identity. This is where the model truly shines.
* Example: "Change the shirt to a kimono with wide sleeves and patterned fabric"
* Example: "Replace the dress with a business suit, navy blue with pinstripes and a silk tie"
#### 2. Human/Character Replacement
Strong results when transforming people into new characters or creatures. Detailed prompts are key for maintaining pose and composition.
* Example: "Replace the person with a tiger, striped orange fur, muscular build, and glowing green eyes"
* Example: "Replace the person with a 2D anime character, big eyes, blue gown and battle scars"
#### 3. Object Replacement
Reliable for structure-preserving swaps. Ideal when replacing one object with another of similar scale.
* Example: "Replace the apple with a glowing crystal ball emitting blue light"
* Example: "Replace the car with a vintage motorcycle, chrome details and leather seat"
### ⚠️ Mixed Reliability
#### 4. Color Changes
Sometimes subtle, sometimes exaggerated. Works best with precise descriptions of material and finish.
* Example: "Change the jacket color to deep red leather with a glossy finish"
* Tip: Specify the exact shade and material properties for better control
#### 5. Adding Objects
Often attaches to the subject rather than appearing independently. Works best for wearable or handheld props.
* Example: "Add a golden crown on the person's head, decorated with ornate jewels"
* Example: "Add sunglasses, aviator style with reflective lenses"
#### 6. Global Transformations
Effective for backgrounds or scene-wide changes, but might alter the subject's identity. Use carefully when subject preservation is important.
* Example: "Transform the sunny beach into a snowy tundra with falling snowflakes"
* Warning: May change the identity or appearance of people in the image
### Additional Optimization Tips
* **Frame count matters**: For video editing, 81-frame generations produce better temporal consistency than shorter clips
* **Detail improves results**: Include style, materials, colors, and textures in your prompts
* **Be explicit about preservation**: If certain elements must remain unchanged, consider mentioning what to keep
**Bottom line:** Lucy Image performs best with detailed prompts that clearly describe the desired changes. Focus on clothing swaps, character replacements, and object substitutions for the most reliable results. Be cautious with global transformations if preserving subject identity is crucial.
## Endpoint and Full API Reference
* **Lucy Image**: [`/v1/generate/lucy-image-2`](/api-reference/lucy-image-2)
## Next steps
Now that you know the basics of image editing, you might want to check out one of these resources next.
Check out all the options for image editing in the API reference.
Explore all non-realtime models.
# Lucy 2.1 Realtime
Source: https://docs.platform.decart.ai/models/realtime/lucy-2.1
Transform yourself into any character in realtime with AI-powered video editing at 720p
Lucy 2.1 is our most advanced realtime video editing model. Upload a reference image and watch yourself transform into that character live. It builds on everything in our original Lucy realtime model and adds **character reference**: provide any face, and Lucy 2.1 maps your movements and expressions onto that identity in real time.
Use Lucy 2.1 as your default realtime editing model. It supports character reference and text-only editing in the same integration.
## Quick start
### Installation
```bash theme={null}
npm install @decartai/sdk
```
```bash theme={null}
pip install decart
```
Add the JitPack repository to your `settings.gradle.kts`:
```kotlin settings.gradle.kts theme={null}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
}
```
Then add the dependency to your app's `build.gradle.kts`:
```kotlin build.gradle.kts theme={null}
dependencies {
implementation("com.github.DecartAI:decart-android:0.2.0")
}
```
### Transform into a character
```typescript theme={null}
import { createDecartClient, models } from "@decartai/sdk";
const model = models.realtime("lucy-2.1");
// Get your camera
const stream = await navigator.mediaDevices.getUserMedia({
video: {
frameRate: model.fps,
width: model.width,
height: model.height,
},
});
const client = createDecartClient({
apiKey: "your-api-key-here",
});
// Connect to Lucy 2.1
const realtimeClient = await client.realtime.connect(stream, {
model,
mirror: "auto",
onRemoteStream: (transformedStream) => {
document.getElementById("output").srcObject = transformedStream;
},
initialState: {
prompt: {
text: "Change the wall's color to light blue, natural consistent paint finish.",
enhance: true,
},
},
});
// Upload a reference image and transform
await realtimeClient.set({
prompt: "Substitute the character in the video with the person in the reference image.",
image: characterImage, // File, Blob, or URL string
enhance: true,
});
```
```python theme={null}
import asyncio
from decart import DecartClient, models
from decart.types import ModelState, Prompt
async def main():
async with DecartClient(api_key="your-api-key-here") as client:
model = models.realtime("lucy-2.1")
realtime = await client.realtime.connect(
stream=media_stream,
model=model,
on_remote_stream=lambda s: display(s),
initial_state=ModelState(
prompt=Prompt(
text="Change the wall's color to light blue, natural consistent paint finish.",
enhance=True,
),
),
)
# Upload a reference image and transform
with open("character.jpg", "rb") as f:
await realtime.set(
prompt="Substitute the character in the video with the person in the reference image.",
image=f,
enhance=True,
)
asyncio.run(main())
```
```kotlin theme={null}
import ai.decart.sdk.DecartClient
import ai.decart.sdk.DecartClientConfig
import ai.decart.sdk.RealtimeModels
import ai.decart.sdk.realtime.ConnectOptions
import ai.decart.sdk.realtime.InitialPrompt
val model = RealtimeModels.LUCY_2_1
val client = DecartClient(context, DecartClientConfig(apiKey = "your-api-key"))
client.realtime.initialize(eglBase)
// Create camera track using model dimensions
val videoSource = client.realtime.createVideoSource(isScreencast = false)!!
val videoTrack = client.realtime.createVideoTrack("camera", videoSource)!!
val enumerator = Camera2Enumerator(context)
val cameraName = enumerator.deviceNames.first { enumerator.isFrontFacing(it) }
val capturer = enumerator.createCapturer(cameraName, null)
capturer.initialize(
SurfaceTextureHelper.create("CaptureThread", client.realtime.getEglBaseContext()),
context,
videoSource.capturerObserver
)
capturer.startCapture(model.width, model.height, model.fps)
// Connect to Lucy 2.1
client.realtime.connect(
localVideoTrack = videoTrack,
options = ConnectOptions(
model = model,
onRemoteVideoTrack = { track ->
remoteRenderer.addSink(track)
},
initialPrompt = InitialPrompt(
text = "Change the wall's color to light blue, natural consistent paint finish.",
enhance = true,
),
)
)
// Upload a reference image and transform
val characterBase64 = Base64.encodeToString(characterBytes, Base64.NO_WRAP)
client.realtime.setImage(
imageBase64 = characterBase64,
prompt = "Substitute the character in the video with the person in the reference image.",
enhance = true
)
```
Pass the prompt and/or reference image in `initialState` so the first frame is already transformed — otherwise viewers briefly see the raw camera feed.
## Portrait mode (9:16)
Lucy 2.1 supports both landscape (16:9) and portrait (9:16) video input. On mobile devices (iOS/Android), portrait mode works automatically — the OS maps the front camera to a vertical stream regardless of the constraints you pass.
For desktop browsers or external webcams, swap `width` and `height` when calling `getUserMedia` to request a portrait stream:
```typescript theme={null}
const model = models.realtime("lucy-2.1");
const stream = await navigator.mediaDevices.getUserMedia({
video: {
frameRate: { ideal: model.fps },
width: { ideal: model.height }, // swap: use height as width
height: { ideal: model.width }, // swap: use width as height
facingMode: "user",
},
});
const client = createDecartClient({ apiKey: "your-api-key-here" });
const realtimeClient = await client.realtime.connect(stream, {
model,
onRemoteStream: (transformedStream) => {
document.getElementById("output").srcObject = transformedStream;
},
});
```
Portrait mode is ideal for mobile webcams and vertical video content. On mobile (iOS/Android), you don't need to swap dimensions — the OS maps the front camera to a vertical stream automatically regardless of the constraints you pass.
## Use cases
Become any character live on camera — no costume needed. Stream as your favorite game, anime, or movie character.
Let customers see products on themselves in realtime. Upload a model photo and map it onto the customer's live feed.
Transform your appearance for Twitch, YouTube, or TikTok Live. Switch characters on the fly without interrupting your stream.
Produce character-driven video content without actors or costumes. Change your look between shots with a single API call.
Appear as a custom avatar in meetings. Your facial expressions and head movements transfer naturally to the reference character.
Build apps where users transform into characters in realtime — photo booths, AR filters, virtual events.
## How character reference works
Lucy 2.1 uses your reference image as a visual identity target. Your live video provides the motion, expressions, and pose — the model blends the two so the output looks like the reference character performing your movements.
Establish a WebRTC connection with your camera stream, just like any other realtime model.
Provide any portrait photo — a character, a celebrity, or a generated face. The model extracts the visual identity from this image.
Your movements, expressions, and gestures are mapped onto the reference character in realtime. The output stream shows the character performing your actions.
### Updating the reference image
You can change the character at any time without reconnecting. Use the `set()` method to atomically replace the session state — include all fields you want to keep:
```typescript JavaScript theme={null}
// Change to a new character
await realtimeClient.set({
prompt: "Substitute the character in the video with the person in the reference image.",
image: newCharacterImage,
enhance: true,
});
// Set a new image only (clears any previous prompt)
await realtimeClient.set({ image: newCharacterImage });
// Set a new prompt only (clears any previous image)
await realtimeClient.set({ prompt: "Add dark sunglasses to the person's face." });
// Clear the reference image (fall back to text-only editing)
await realtimeClient.set({ image: null });
```
```python Python theme={null}
# Change to a new character
with open("new_character.jpg", "rb") as f:
await realtime.set(
prompt="Substitute the character in the video with the person in the reference image.",
image=f,
enhance=True,
)
# Set a new image only (clears any previous prompt)
with open("another.jpg", "rb") as f:
await realtime.set(image=f)
# Set a new prompt only (clears any previous image)
await realtime.set(prompt="Add dark sunglasses to the person's face.")
```
```kotlin Android theme={null}
// Change to a new character
val newCharacterBase64 = Base64.encodeToString(newCharacterBytes, Base64.NO_WRAP)
client.realtime.setImage(
imageBase64 = newCharacterBase64,
prompt = "Substitute the character in the video with the person in the reference image.",
enhance = true
)
// Set a new image only
client.realtime.setImage(imageBase64 = newCharacterBase64)
// Set a new prompt only (text-only editing)
client.realtime.setPrompt("Add dark sunglasses to the person's face.")
// Clear the reference image (fall back to text-only editing)
client.realtime.setImage(imageBase64 = null)
```
`set()` replaces the entire state — fields you omit are cleared. Always include every field you want to keep. This avoids intermediate states and ensures prompt and image stay in sync.
## Text-only editing
Lucy 2.1 works without a reference image too. Use text prompts to add, modify, or remove elements in your live video:
```typescript JavaScript theme={null}
// No reference image needed for text-only edits
await realtimeClient.set({ prompt: "Add a small dog running around in the background." });
await realtimeClient.set({ prompt: "Change the background to a sandy beach with clear blue water." });
await realtimeClient.set({ prompt: "Change the person's hair color to bright blonde." });
```
```python Python theme={null}
# No reference image needed for text-only edits
await realtime.set(prompt="Add a small dog running around in the background.")
await realtime.set(prompt="Change the background to a sandy beach with clear blue water.")
await realtime.set(prompt="Change the person's hair color to bright blonde.")
```
```kotlin Android theme={null}
// No reference image needed for text-only edits
client.realtime.setPrompt("Add a small dog running around in the background.")
client.realtime.setPrompt("Change the background to a sandy beach with clear blue water.")
client.realtime.setPrompt("Change the person's hair color to bright blonde.")
```
See the [prompting guide](#prompting-guide) below for the best prompt structures for each edit type.
## Prompting guide
Lucy 2.1 responds best when your prompts follow specific patterns for each edit type. There are four supported operations, each with its own structure.
| Edit type | Prompt template |
| - | - |
| **Character transformation** | "Substitute the character in the video with \." |
| **Add** | "Add \ to \." |
| **Replace** | "Change \