3D models
POST /api/v1/3d — GLB, USDZ and FBX of any vehicle in any paint; status, webhooks, downloads.
POST /api/v1/3d turns any catalog vehicle, in any paint, into a textured 3D model: GLB, USDZ, FBX and a thumbnail. Models are built from the same renders the image API serves, so they match the pictures you already show, and every color of a vehicle is one mesh with a different texture. One request costs 1,000 credits ($1.00), charged at creation whether the model is cached, in progress or new; polling, downloads and webhooks are free.
Two models, one mesh
A 2025 Toyota Camry, made by the endpoint below in silver and then retextured to red: wheels, glass and trim untouched, only the panels repainted. Drag to orbit. Each file is about 1 MB here because it is meshopt-compressed with WebP textures for the page; the API delivers the full-resolution originals listed under Files.

Same mesh, retextured per color.
Endpoints
/api/v1/3d/api/v1/3d/{id}/api/v1/3d/{id}/files/{kind}/api/v1/3d?limit=Create a model
/api/v1/3d| Parameter | Type | Description |
|---|---|---|
makerequired | string | Make name or slug, as on GET /api/v1/images/car. Omit when you send vehicle. |
modelrequired | string | Model name or slug. Omit when you send vehicle. |
yearrequired | integer | Model year. Omit when you send vehicle. |
vehicle | string | A stable vehicle id ( veh_…) in place of make, model and year. Sending both is a 400; an unknown id is a 404. |
color | preset | hex | One of the 15 preset names, or any hex ( "#1a2b3c" or "1a2b3c"; a hex equal to a preset swatch is that preset). Echoed as the preset name or #1a2b3c. Every color of a vehicle shares one mesh, so a second color is a retexture, not a rebuild.Default: silver |
webhook_url | string (https) | A public HTTPS URL that receives one POST when the model is ready or failed. See Webhooks. |
webhook_secret | string | Signs the webhook body (HMAC-SHA256). Only with webhook_url; never echoed. |
| Header | Example | Meaning |
|---|---|---|
Idempotency-Key | 3d-toyota-camry-2025-red | Optional but recommended: the same key and body within 24 hours replays the first response instead of charging again, exactly as on POST /api/v1/image-urls. |
curl --fail-with-body \
-X POST https://carimage.dev/api/v1/3d \
-H "Authorization: Bearer $CAR_IMAGE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 3d-toyota-camry-2025-red" \
-d '{"make":"toyota","model":"camry","year":2025,"color":"red"}'{
"data": {
"id": "0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10",
"object": "3d_model",
"status": "processing",
"progress": 35,
"stage": "geometry",
"vehicle": {
"id": "veh_errc87t1jgata",
"make": "toyota",
"model": "camry",
"year": 2025
},
"color": "red",
"generator": "meshy-7",
"files": null,
"polycount": null,
"error": null,
"estimated_seconds_remaining": 170,
"webhook": null,
"created_at": "2026-09-16T09:12:04.000Z",
"updated_at": "2026-09-16T09:12:40.000Z",
"ready_at": null
},
"billing": {
"charged_on": "creation",
"credits_charged": 1000,
"credits_remaining": 3870
},
"request_id": "req_01j9x…"
}id is your request, scoped to your account; the shared model behind it is never exposed. When another customer, or you, already asked for this vehicle in this color, the answer is 200 with status: "ready" and the files straight away. A Location header points at /api/v1/3d/{id} either way.
Status lifecycle
| status | Meaning | Then |
|---|---|---|
| queued | Accepted and charged; the source views are being rendered or the provider has not picked the task up yet. | processing |
| processing | The mesh is being built (stage geometry) or painted (stage texture). progress climbs from 0 to 100. | ready or failed |
| ready | files holds every format; ready_at is set; the webhook, if any, has been queued. | final |
| failed | error carries a code and a message; the credits were refunded. Ask again and a fresh attempt is made. | final |
The first model of a vehicle takes 3–5 minutes: the four orthogonal views are rendered (stage views), the mesh is built from them (geometry) and textured (texture). Every further color of that vehicle reuses the mesh and only repaints it, about 1–2 minutes. progress is 0–100 and estimated_seconds_remaining is a rough guide for a progress bar, not a promise; it is null once the model is ready or failed.
Poll or list
/api/v1/3d/{id}Returns the same data object, refreshed. While the model is being made, polling every 10–15 seconds is plenty: the server re-reads the generator when its last look is older than 15 seconds, so a tighter loop only sees the same answer. Once status is ready, files carries a URL, byte count and content type per format.
{
"data": {
"id": "0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10",
"object": "3d_model",
"status": "ready",
"progress": 100,
"stage": "done",
"vehicle": {
"id": "veh_errc87t1jgata",
"make": "toyota",
"model": "camry",
"year": 2025
},
"color": "red",
"generator": "meshy-7",
"files": {
"glb": {
"url": "https://carimage.dev/api/v1/3d/0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10/files/glb",
"bytes": 6412309,
"content_type": "model/gltf-binary"
},
"usdz": {
"url": "https://carimage.dev/api/v1/3d/0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10/files/usdz",
"bytes": 7204118,
"content_type": "model/vnd.usdz+zip"
},
"fbx": {
"url": "https://carimage.dev/api/v1/3d/0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10/files/fbx",
"bytes": 6530772,
"content_type": "application/octet-stream"
},
"thumbnail": {
"url": "https://carimage.dev/api/v1/3d/0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10/files/thumbnail",
"bytes": 214907,
"content_type": "image/png"
}
},
"polycount": 59842,
"error": null,
"estimated_seconds_remaining": null,
"webhook": null,
"created_at": "2026-09-16T09:12:04.000Z",
"updated_at": "2026-09-16T09:13:21.000Z",
"ready_at": "2026-09-16T09:13:21.000Z"
},
"request_id": "req_01j9x…"
}/api/v1/3d?limit=Your recent requests, newest first, as data: [ … ] of the same objects. limit is 1–50 (default 20); the response also carries pricing.credits_per_model, the current price of a request.
Files
/api/v1/3d/{id}/files/{kind}| kind | Content-Type | Typical size | Use |
|---|---|---|---|
glb | model/gltf-binary | about 6 MB | Web viewers (<model-viewer>, three.js), Blender, Unity, Unreal, game engines. |
usdz | model/vnd.usdz+zip | about 7 MB | iOS AR Quick Look and visionOS; ios-src on <model-viewer>. |
fbx | application/octet-stream | similar to the GLB | Maya, 3ds Max and pipelines that still want FBX. |
thumbnail | image/png | a few hundred KB | A transparent still of the model for lists and posters. |
Each file URL answers 302 with a signed URL on our storage that is valid for one hour and needs no key; curl -L and every download tool follow it. Fetch with your API key from a server, then either store the file with your other assets or hand the signed Location to a browser for the hour. Before the model is ready the URL answers 409 with Retry-After: 15; a kind outside glb, usdz, fbx, thumbnail is a 404.
curl -s -H "Authorization: Bearer $CAR_IMAGE_API_KEY" https://carimage.dev/api/v1/3d/0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10
# -> data.status: "ready", data.files.glb.url, data.files.usdz.url, …
curl -L --fail-with-body \
-H "Authorization: Bearer $CAR_IMAGE_API_KEY" \
https://carimage.dev/api/v1/3d/0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10/files/glb \
--output camry-2025-red.glbWebhooks
Pass webhook_url and the endpoint POSTs once when the model settles, so nothing has to poll. The body is the same JSON GET /api/v1/3d/{id} returns, wrapped in an event:
{
"event": "3d_model.ready",
"data": {
"id": "0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10",
"object": "3d_model",
"status": "ready",
"progress": 100,
"stage": "done",
"vehicle": {
"id": "veh_errc87t1jgata",
"make": "toyota",
"model": "camry",
"year": 2025
},
"color": "red",
"generator": "meshy-7",
"files": {
"glb": {
"url": "https://carimage.dev/api/v1/3d/0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10/files/glb",
"bytes": 6412309,
"content_type": "model/gltf-binary"
},
"usdz": {
"url": "https://carimage.dev/api/v1/3d/0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10/files/usdz",
"bytes": 7204118,
"content_type": "model/vnd.usdz+zip"
},
"fbx": {
"url": "https://carimage.dev/api/v1/3d/0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10/files/fbx",
"bytes": 6530772,
"content_type": "application/octet-stream"
},
"thumbnail": {
"url": "https://carimage.dev/api/v1/3d/0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10/files/thumbnail",
"bytes": 214907,
"content_type": "image/png"
}
},
"polycount": 59842,
"error": null,
"estimated_seconds_remaining": null,
"webhook": null,
"created_at": "2026-09-16T09:12:04.000Z",
"updated_at": "2026-09-16T09:13:21.000Z",
"ready_at": "2026-09-16T09:13:21.000Z"
},
"sent_at": "2026-09-16T09:13:22.000Z"
}| Header | Example | Meaning |
|---|---|---|
X-CarImage-Event | 3d_model.ready | 3d_model.failed | The event, also in the body. |
X-CarImage-Delivery | 5c1d… | Unique per delivery attempt; use it to ignore a duplicate. |
X-CarImage-Signature | sha256=9f2a… | Only with webhook_secret: hex HMAC-SHA256 of the raw request body, keyed with the secret you sent. |
Answer with any 2xx within 10 seconds. A non-2xx or a timeout is retried with backoff up to five times, each with a new X-CarImage-Delivery. The URL must be HTTPS on a public host (no loopback, private ranges or internal names); an invalid one is a 400 before anything is charged. Verify the signature over the raw bytes before parsing:
import { createHmac, timingSafeEqual } from "node:crypto";
// Read the raw body: the signature covers the exact bytes we sent.
export async function POST(request: Request) {
const body = await request.text();
const expected = "sha256=" + createHmac("sha256", process.env.CAR_IMAGE_WEBHOOK_SECRET).update(body).digest("hex");
const given = request.headers.get("x-carimage-signature") ?? "";
if (given.length !== expected.length || !timingSafeEqual(Buffer.from(given), Buffer.from(expected))) {
return new Response("bad signature", { status: 401 });
}
const { event, data } = JSON.parse(body);
if (event === "3d_model.ready") {
// data is the same JSON GET /api/v1/3d/{id} returns: data.files.glb.url etc.
}
return new Response(null, { status: 204 }); // any 2xx stops the retries
}Download and embed
Download the GLB (and the USDZ for iOS) with your key, host them next to your other assets and show them with Google's <model-viewer>, the same element the demo above uses. The thumbnail makes a good poster while the model loads.
<script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/4.1.0/model-viewer.min.js"></script>
<model-viewer
src="/models/camry-2025-red.glb"
ios-src="/models/camry-2025-red.usdz"
poster="/models/camry-2025-red.png"
alt="2025 Toyota Camry, red"
camera-controls auto-rotate ar
shadow-intensity="1"
style="width: 100%; aspect-ratio: 1"
></model-viewer>Pricing
- 1,000 credits ($1.00) per request, charged at creation: cached, in progress or new, the price is the same, exactly as with images. An
Idempotency-Keymakes a retried request replay instead of charging twice. - Polling, listing, downloads and webhook deliveries are free.
- A model that fails is refunded in full; ask again and a fresh attempt is made.
402when the balance is short of 1,000 credits (withbalanceandrequired_credits); nothing is charged.503withretry_after_seconds(andRetry-After) when 3D generation is at its daily capacity or switched off; nothing is charged, images are unaffected, and the request succeeds again once the window resets.
Errors
400— a bad body: unknown field,vehicletogether with make, model and year, an unparseablecolor, awebhook_urlthat is not public HTTPS, or awebhook_secretwithout a URL.402— fewer than 1,000 credits. Do not retry; top up.404— the vehicle is not in the catalog, no vehicle has that id, or no request of yours has that model id.409— a file was asked for before the model was ready (Retry-After), or anIdempotency-Keyretry overtook the first request.503— at capacity or disabled; waitretry_after_seconds.
Every failure is application/problem+json. See Errors.