Visualizee REST API
Render images and 3D models from your own code with a static API key. One POST submits a render, one GET polls the result. Available on the Pro and Max plans and enterprise tiers.
Visualizee has a small REST API for the cases where a chat assistant is the wrong shape: your own scripts, a batch job over a product catalogue, a CMS plugin, or a pipeline that renders on a schedule. You authenticate with a static API key, POST a plain-language prompt plus up to four images, and poll one endpoint for the result. The same key also turns one to four images of an object into a textured 3D model.
The base URL is on the app domain, not this site:
https://visualizee.app/api/v1
Every request runs a headless Vizzy turn - the same assistant as the app chat, with the same system prompt, prompt enhancement, and brand and policy rules. You describe what you want; Vizzy writes the render prompt, fills in what you did not pin, and submits the render. 3D models skip Vizzy: you send the views and get the model. Version 1 renders images and 3D models; a /render/video endpoint will follow.
Rendering every SKU in a catalogue into the same studio setup, generating a hero image for each new blog post from a build script, or dropping a "restyle this photo" button into your own product. If you want to render by talking to an assistant instead, use the MCP server.
Get an API key
API keys are available on the Pro and Max plans and custom enterprise tiers.
Open the API section
Sign in at visualizee.app, open Account, and scroll to the API section.
Create a key
Click Create key and give it a name, for example "catalogue pipeline". The key starts with vz_live_ and is shown once - copy it now and store it as a server-side secret.
Manage it later
The same section lists each key with its renders, spend, and last-used time. Revoke a key at any time; requests with a revoked key answer 401 unauthorized immediately.
A key spends from your plan. Never ship it in a browser bundle, a mobile app, or a public repository - call the API from your server and pass results on. If a key leaks, revoke it on the account page and create a new one; per-key stats show whether anything was rendered in the meantime.
Authentication
Send the key as a bearer token on every request:
Authorization: Bearer vz_live_...
A missing, malformed, or revoked key answers 401 unauthorized. A valid key on a plan without API access answers 403 api_access_not_included, and a key on an account whose access has lapsed answers 403 account_access_expired.
Render an image
POST https://visualizee.app/api/v1/render/image
Content-Type: application/json
The call is synchronous up to the point of submission: it runs the Vizzy turn, which can take up to about 60 seconds, then returns the ids of the renders it submitted. It does not wait for the images themselves - poll the status endpoint for those.
Request body
Unknown fields are rejected with 422 invalid_request.
| Field | Type | Default | Notes |
|---|---|---|---|
prompt | string | required | 1 to 4,000 characters of plain language. Refer to attached images as |
images | array | none | Up to 4 items, each either |
max_renders | integer | 1 | 1 to 16. An upper bound on how many images the request may produce. Vizzy renders one image
unless the prompt clearly asks for several (variations, angles) and never pads to the
maximum. Must not exceed your plan's batch cap (Pro: 8, Max: 16), or the call answers
|
model_id | uuid | best model for the plan | Pins an image model. Get the ids from |
aspect_ratio | string | "auto" |
|
resolution | integer | 1024 | Pixel budget for a 1:1 image; other ratios keep the same pixel count. Capped by your plan (Max is uncapped) and by the values the model supports. |
background | string | none |
|
customer | string | none | Your own id for the end customer this render is for: a shop, a tenant, a user of your
app. 1 to 128 letters, digits, or |
Attaching images
Each entry in images is one of:
{ "url": "https://..." }- a publichttporhttpsURL. Private and loopback hosts are rejected.{ "base64": "..." }- raw base64 or adata:URL. JPEG, PNG, or WebP, up to 8 MB decoded.
Every image is downloaded or decoded, normalized (EXIF rotation applied, long edge resized to 2048px, re-encoded as a JPEG of roughly 300 KB), and re-hosted before the render starts. An image that cannot be fetched or decoded answers 422 invalid_image with the offending index in details, and nothing is charged.
The whole request body, base64 payloads included, must stay under 4.5 MB. For larger files, or more than one or two photos, host them somewhere public and pass URLs instead.
In the prompt, refer to attached images by position: image[0] is the first entry, image[1] the second. image[0] is the base image to edit unless the prompt says otherwise; the others are references for style, materials, or parts. The same preserve/change contract that works in the app works here:
"Place image[0] in a dark photo studio with soft rim light. Keep the car's body, paint, and stance exactly as they are; match the wheel style from image[1]."
With no images attached, the prompt is a text-to-image request.
Response
201 Created:
{
"summary": "Placed the coupe in a charcoal studio with soft rim light.",
"customer": null,
"renders": [{ "id": "6e1f3d2a-9b7c-4e1d-8f55-2a0c7b9e4d11", "status": "pending" }],
"credits_charged": 80,
"credits_remaining": 311920
}
summary is Vizzy's one-to-two sentence description of what it rendered. renders holds one entry per submitted render, up to max_renders. You are charged at this point; see Cost and refunds below.
Make a 3D model
POST https://visualizee.app/api/v1/render/3d
Content-Type: application/json
Turns one to four images of one object (a game prop, a character, a product, a piece of furniture) into a textured 3D model you download as a GLB. There is no Vizzy turn and no prompt: you send the views, the call returns a render id within a few seconds, and the model is ready in 2 to 4 minutes on the status endpoint or your webhook.
Request body
Unknown fields are rejected with 422 invalid_request.
| Field | Type | Default | Notes |
|---|---|---|---|
images | array | required | 1 to 4 views of the same object, each |
quality | string | "pro" |
|
model_id | uuid | none | Pins a 3D model from |
customer | string | none | Your own end-customer id, as on /render/image. |
Images are fetched, normalized and re-hosted exactly as for /render/image. What makes a good model is the input:
- One object, whole, on a plain background. No rooms, scenes or UI screens: render the single object on its own first (for example with
/render/image, asking to isolate it on a plain light background). - Same object, same size, same light in every view. Views that disagree (a handle on the left in one, on the right in another) give the model two answers to choose between.
- One view works, but the back is a guess. A front, a side and a back view give a model that follows them.
curl -s https://visualizee.app/api/v1/render/3d \
-H "Authorization: Bearer $VISUALIZEE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"images": [
{ "url": "https://example.com/chest-front.png", "role": "front" },
{ "url": "https://example.com/chest-side.png", "role": "left" },
{ "url": "https://example.com/chest-back.png", "role": "back" }
],
"quality": "standard"
}'
201 Created:
{
"summary": "3D model from 3 views (front, left, back) started on Tripo 3D. It takes 2-4 minutes; poll GET /render/status/{id} or wait for the webhook.",
"customer": null,
"model": "Tripo 3D",
"quality": "standard",
"views": ["front", "left", "back"],
"renders": [{ "id": "47105ae2-aa29-4155-b8e2-69d328d99b88", "status": "pending" }],
"credits_charged": 300,
"credits_remaining": 201183
}
When it completes, the status endpoint (and the render.completed webhook) carries type: "model_3d", a preview render in image_url, and the files in model_3d (see Check a render). 3D models are on the Pro and Max plans; the price is the same for one view or four.
Check a render
GET https://visualizee.app/api/v1/render/status/{id}
Returns the state of one render. Poll every few seconds - images usually finish in under a minute. A render owned by another account answers 404 not_found.
{
"id": "6e1f3d2a-9b7c-4e1d-8f55-2a0c7b9e4d11",
"type": "image",
"status": "completed",
"customer": "shop_42",
"image_url": "https://storage.visualizee.app/renders/6e1f3d2a.jpg",
"model_3d": null,
"width": 1536,
"height": 1024,
"model": "Grok Imagine 2.0",
"credits": 80,
"refunded": false,
"error": null,
"created_at": "2026-09-02T12:00:00Z",
"completed_at": "2026-09-02T12:00:41Z"
}
A finished 3D model looks like this (3D models take 2 to 4 minutes, so poll every 10 to 15 seconds):
{
"id": "47105ae2-aa29-4155-b8e2-69d328d99b88",
"type": "model_3d",
"status": "completed",
"customer": null,
"image_url": "https://storage.visualizee.app/files/47105ae2-preview.png",
"model_3d": {
"glb_url": "https://storage.visualizee.app/files/47105ae2.glb",
"glb_light_url": null,
"glb_bytes": 6210040,
"views": ["front", "left", "back"]
},
"model": "Tripo 3D",
"credits": 300,
"refunded": false,
"error": null,
"created_at": "2026-10-21T12:00:00Z",
"completed_at": "2026-10-21T12:03:11Z"
}
| Field | Notes |
|---|---|
type | image, video, or model_3d. |
status | pending, completed, or failed. |
customer | The customer sent with the request, or null. |
image_url |
|
model_3d | 3D models only, |
width, height | null until completed. |
model | The display name of the model that rendered it. |
credits, refunded | What the render cost, and whether that charge has been returned after a failure. |
error | null, or the engine's message when status is failed. |
List models
GET https://visualizee.app/api/v1/models
Returns the image models you can pin with model_id, best first, with the cost of a 1024 px square render and the ratios, resolutions, and input-image count each one supports. The entry marked default: true is the model used when model_id is omitted.
{
"models": [
{
"id": "8a8a2a68-a43f-4818-83ba-a49ad0cb004a",
"name": "Grok Imagine 2.0",
"tier": "base",
"default": true,
"credits_at_1024": 80,
"aspect_ratios": ["1:1", "4:3", "3:4", "16:9", "9:16"],
"resolutions": [1024, 2048],
"max_input_images": 4,
"transparent_background": false
}
]
}
tier is base or pro. transparent_background says whether the model can render background: "transparent". Pro renders count against the plan's monthly pro-render quota; the models reference describes what each model is good at.
The same response lists the 3D models for /render/3d under models_3d, Pro first:
{
"models": ["..."],
"models_3d": [
{
"id": "8e3778ad-71b2-4a1f-915d-9c41f301fe4d",
"name": "Hunyuan 3D Pro",
"quality": "pro",
"default": true,
"credits": 900,
"max_input_images": 4
},
{
"id": "1430e417-f04b-4417-b8fb-37144de92c09",
"name": "Tripo 3D",
"quality": "standard",
"default": false,
"credits": 300,
"max_input_images": 4
}
]
}
Full example with curl
Submit a render that restyles a product photo, then poll until it finishes:
export VISUALIZEE_API_KEY="vz_live_..."
# 1. Submit
curl -s https://visualizee.app/api/v1/render/image \
-H "Authorization: Bearer $VISUALIZEE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Place image[0] in a bright Scandinavian living room, morning light from the left. Keep the chair exactly as it is.",
"images": [{ "url": "https://example.com/chair.jpg" }],
"aspect_ratio": "3:2"
}'
# -> { "summary": "...", "renders": [{ "id": "6e1f...", "status": "pending" }], ... }
# 2. Poll (repeat every few seconds until status is "completed" or "failed")
curl -s https://visualizee.app/api/v1/render/status/6e1f3d2a-9b7c-4e1d-8f55-2a0c7b9e4d11 \
-H "Authorization: Bearer $VISUALIZEE_API_KEY"
To send a local file instead of a URL, base64-encode it into the body:
curl -s https://visualizee.app/api/v1/render/image \
-H "Authorization: Bearer $VISUALIZEE_API_KEY" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg img "$(base64 -i chair.jpg)" '{
prompt: "Place image[0] on a concrete plinth in a white studio.",
images: [{ base64: $img }]
}')"
Node example
The same flow with fetch, no dependencies:
const API = 'https://visualizee.app/api/v1';
const headers = {
Authorization: `Bearer ${process.env.VISUALIZEE_API_KEY}`,
'Content-Type': 'application/json',
};
const submit = await fetch(`${API}/render/image`, {
method: 'POST',
headers,
body: JSON.stringify({
prompt:
'Place image[0] in a dark photo studio with soft rim light, three-quarter front view. Keep the car exactly as it is.',
images: [{ url: 'https://example.com/car.jpg' }],
aspect_ratio: '3:2',
}),
});
if (!submit.ok) {
const { error } = await submit.json();
throw new Error(`${error.code}: ${error.message}`);
}
const { summary, renders } = await submit.json();
console.log(summary);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
for (const { id } of renders) {
let render;
do {
await sleep(4000);
render = await fetch(`${API}/render/status/${id}`, { headers }).then((r) => r.json());
} while (render.status === 'pending');
if (render.status === 'completed') console.log(render.image_url);
else console.error(`Render ${id} failed: ${render.error}`);
}
Retries and idempotency
A render request runs for up to a minute, so a dropped connection can leave you unsure whether it went through. Send an Idempotency-Key header (any unique string up to 255 printable characters, a UUID works well) and retry with the same key:
POST https://visualizee.app/api/v1/render/image
Authorization: Bearer vz_live_...
Idempotency-Key: 3b8f0c1e-6a5d-4f7e-9c2b-8d1a4e6f0b7c
- Same key, same body, within 24 hours: the original response is returned, with an
Idempotent-Replayed: trueheader. No second Vizzy turn, no second charge. Errors are replayed too; to try again after an error, use a new key. - Same key while the first request is still running:
409 idempotency_key_in_use. Wait and retry. - Same key, different body:
422 idempotency_key_reused. - The original request never finished (it was cut off on our side):
409 idempotency_request_interrupted. Check the API folder in your workspace before retrying with a new key.
Requests without the header are not deduplicated.
Concurrency
Each API key may have 10 render requests in flight at once. An eleventh answers 429 concurrency_limit with a Retry-After header (seconds); retry when one of the running requests returns. Renders that were already submitted and are still being made by the engine do not count, only requests still waiting for their response. For a higher cap, contact support.
For large batches, run a fixed pool of workers (up to 10 per key) that each send one request at a time, rather than firing everything at once.
Webhooks
Instead of polling, register one HTTPS endpoint per API key. Visualizee sends a signed event for every render that key creates as soon as it finishes.
Set the endpoint
PUT https://visualizee.app/api/v1/webhook
Authorization: Bearer vz_live_...
Content-Type: application/json
{ "url": "https://example.com/hooks/visualizee" }
{
"url": "https://example.com/hooks/visualizee",
"secret": "whsec_5c1e...",
"created_at": "2026-10-15T12:00:00Z",
"updated_at": "2026-10-15T12:00:00Z"
}
The secret is returned only when it is created: on the first PUT, or when you send "rotate_secret": true. Store it as a server-side secret. Changing the URL later keeps the same secret. The endpoint must be https on a public host.
GET /api/v1/webhookreturns the current URL (never the secret).DELETE /api/v1/webhookremoves it.POST /api/v1/webhook/testsends a signedpingevent and reports the HTTP status your endpoint answered.
Events
render.completed and render.failed, one per render (images and 3D models alike), as a POST with a JSON body. data has exactly the fields of the status endpoint, type and model_3d included:
{
"id": "evt_0b6f3c1a-2d4e-4f8a-9b7c-1e2d3c4b5a69",
"type": "render.completed",
"created_at": "2026-10-15T12:00:41Z",
"data": {
"id": "6e1f3d2a-9b7c-4e1d-8f55-2a0c7b9e4d11",
"type": "image",
"status": "completed",
"customer": "shop_42",
"image_url": "https://storage.visualizee.app/renders/6e1f3d2a.jpg",
"model_3d": null,
"width": 1536,
"height": 1024,
"model": "Grok Imagine 2.0",
"credits": 80,
"refunded": false,
"error": null,
"created_at": "2026-10-15T12:00:00Z",
"completed_at": "2026-10-15T12:00:41Z"
}
}
Answer with any 2xx within 5 seconds. A timeout, a network error, a 408, 429, or 5xx is retried twice (after about 2 and 10 seconds); any other status is taken as final. Delivery is best effort: if your endpoint was down, poll the status endpoint for renders you never heard about. Events can arrive more than once and out of order, so key your handling on data.id.
Verify the signature
Every event carries a Visualizee-Signature header: t=<unix seconds>,v1=<hex HMAC-SHA256>. The HMAC key is your webhook secret and the signed message is <t>.<raw request body>. Recompute it over the raw body, compare in constant time, and reject timestamps more than 5 minutes old:
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyVisualizeeSignature(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const received = Buffer.from(parts.v1 ?? '', 'hex');
return received.length === 32 && timingSafeEqual(received, Buffer.from(expected, 'hex'));
}
Usage by customer
GET https://visualizee.app/api/v1/usage?from=2026-10-01&to=2026-10-31
Totals for the calling key, grouped by the customer you sent with each request (null for untagged renders). from and to are UTC dates and to is inclusive; without them, the current month so far. The range may cover up to 92 days. Add &customer=shop_42 to get one customer only. Failed renders are refunded, so they count in failed but not in credits.
{
"from": "2026-10-01T00:00:00.000Z",
"to": "2026-11-01T00:00:00.000Z",
"totals": { "renders": 412, "failed": 3, "credits": 32720 },
"customers": [
{ "customer": "shop_42", "renders": 300, "failed": 2, "credits": 23840 },
{ "customer": null, "renders": 112, "failed": 1, "credits": 8880 }
]
}
Cost and refunds
- A render counts against your plan the same way as in the app. The price depends on the model and resolution; see plans and renders and the models page.
GET /api/v1/modelsreturns each model's cost. For scale: a 1K image costs 80 credits on Base, 150 on Pro and 300 on Ultra, a 3D model 300 on Standard and 900 on Pro, and Pro includes 19,250 credits a month, Max 48,000. - You are charged when the render is submitted, and the response reports
credits_chargedandcredits_remaining. - A render that fails on the engine is refunded automatically, and the status endpoint shows
refunded: truefor it. - A request that never reaches a render - a validation error, a plan limit, a bad image, or a policy refusal - is never charged.
- If the account cannot cover the request, the call answers
402 insufficient_creditswithrequiredandavailableindetails, and nothing is submitted.
The Vizzy turn itself is free; you pay for renders only.
Errors
Every error is a JSON envelope with a stable code you can branch on, a human-readable message, and sometimes details:
{
"error": {
"code": "insufficient_credits",
"message": "This request needs 160 credits; 40 are available.",
"details": { "required": 160, "available": 40 }
}
}
| HTTP | Code | Meaning |
|---|---|---|
| 400 | invalid_json | The body is not valid JSON. |
| 401 | unauthorized | Missing, malformed, or revoked API key. |
| 402 | insufficient_credits | Not enough left on your plan. details has required and available. |
| 403 | api_access_not_included | The account's plan does not include the API. |
| 403 | account_access_expired | The account's access has lapsed. |
| 404 | not_found | No such render, or it belongs to another account (status endpoint). |
| 404 | webhook_not_configured | POST /webhook/test on a key without a webhook. |
| 409 | idempotency_key_in_use | A request with this Idempotency-Key is still running. |
| 409 | idempotency_request_interrupted | The original request with this key never finished. Check the workspace, then retry with a new key. |
| 422 | invalid_request | Schema validation failed. |
| 422 | invalid_idempotency_key | The Idempotency-Key header is empty, too long, or has spaces or non-ASCII characters. |
| 422 | idempotency_key_reused | This Idempotency-Key was already used with a different body. |
| 422 | invalid_image | An image could not be fetched or decoded. details.index says which one. |
| 422 | too_many_images | More than 4 images. |
| 422 | unknown_model | model_id does not match any model. |
| 422 | invalid_model | model_id is the wrong kind for the endpoint: a video or 3D model on /render/image, anything but a 3D model on /render/3d. |
| 422 | invalid_parameter | A value the model does not support, for example an aspect ratio it cannot render. |
| 422 | background_unsupported | A transparent background on a model that renders it opaque, sent as |
| 422 | plan_limit | The request exceeds a plan limit, for example resolution, or 3D on a plan without it. |
| 422 | invalid_views |
|
| 422 | max_renders_above_plan | max_renders is above the plan's batch cap. |
| 422 | no_render | Vizzy finished the turn without rendering, for example a request that cannot be made safe. |
| 429 | concurrency_limit | The key already has its maximum of requests in flight. Retry after the |
| 504 | timeout | The Vizzy turn did not finish within the request budget. Retry with the same
|
| 500 | internal_error | Something failed on our side. Retry, and contact support if it persists. |
Because every request goes through Vizzy, the same content rules as the app apply. A named real person or a copyrighted character is not recreated - Vizzy swaps it for a generic description and renders that, and the summary tells you what it did. Requests that cannot be made safe at all end without a render and answer 422 no_render with Vizzy's explanation in message, and cost nothing.
Limits
| Limit | Value |
|---|---|
| Prompt length | 1 to 4,000 characters |
| Images per request | 4 |
| Image size | 8 MB decoded; JPEG, PNG, or WebP |
| Request body | 4.5 MB in total, base64 included |
| Views per 3D model | 1 to 4, one object; 2 to 4 minutes to finish |
| Renders per request | max_renders up to 8 on Pro, 16 on Max |
| Resolution | Uncapped on Pro and Max; otherwise the plan's image ceiling |
| Submission time | Up to about 60 seconds for the Vizzy turn, then 504 timeout |
| Concurrent requests | 10 in flight per key, then 429 concurrency_limit |
| Idempotency window | 24 hours per Idempotency-Key |
| Webhooks | One HTTPS endpoint per key, 5 s per delivery attempt, 3 attempts |
Not in v1 yet: video, and a test mode that returns placeholder images without spending credits.
MCP or REST API: which one
Both surfaces render with your account and file results into your workspace. They differ in who is doing the calling:
| MCP server | REST API | |
|---|---|---|
| Who calls it | An AI assistant (Claude, Claude Code, Cursor, Lovable) on your behalf | Your own code, scripts, and pipelines |
| Auth | OAuth 2.1, sign in once in the browser, no keys | Static vz_live_ API key in a bearer header |
| Plans | Every plan | Pro, Max and enterprise |
| Video | Yes, render_video | Not yet |
| 3D models | Yes, render_3d_model (Pro and Max) | Yes, POST /render/3d |
| Best for | Interactive work, one-off renders while you write or build | Unattended, repeatable, high-volume rendering |
If a person is in the loop and talking to an assistant, use MCP. If a machine is in the loop, use the REST API.
OpenAPI spec
A machine-readable description of every endpoint, the request and response schemas, the bearer auth scheme, and the error envelope:
https://visualizee.ai/openapi/v1.json
Feed it to a client generator, an API explorer, or an agent that builds its own integration.