3D models
POST /api/v1/3d — GLB, USDZ and FBX of any vehicle in any paint; own it once, publish it as a two-line embed.
POST /api/v1/3d turns any catalog vehicle, in any paint, into a textured 3D model: GLB, USDZ, FBX, a thumbnail and a browser build of the GLB. 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 model costs 100 credits, charged once, from the credits a plan includes every month (or the signup credits): a vehicle and color you already own is free to order again. Polling, downloads and webhooks are free, and publishing gives any model key-free URLs and a two-line embed hosted by Car Image.
The Porsche from the pictures, in 3D
The 2024 Porsche 911 from our image examples, made with the API’s generation recipe in red, blue and silver. Switch colors to compare: the shape, glass, trim and wheels stay the same. Load it and drag to orbit. This is its browser build (glb_web), about 29.3 MB: every triangle of the reconstruction, meshopt-compressed, with a lossless WebP paint texture and the provider’s PBR maps; the API also delivers the full-resolution originals listed under Files.

Choose a color. Only the paint changes.
Endpoints
/api/v1/3d/api/v1/3d/{id}/api/v1/3d/{id}/files/{kind}/api/v1/3d?limit=/api/v1/3d/{id}/publish/api/v1/3d/{id}/publish/api/v1/3d/public/{public_id}/api/v1/3d/public/{public_id}/{file}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 finished mesh, so a second color changes only the paint, not the mesh.Default: silver |
publish | boolean | true publishes the model at once: data.public carries the key-free URLs and the embed, whose files appear when the model is ready. See Host it anywhere. Free.Default: false |
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-porsche-911-2024-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-porsche-911-2024-red" \
-d '{"make":"porsche","model":"911","year":2024,"color":"red","publish":true}'{
"data": {
"id": "0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10",
"object": "3d_model",
"status": "processing",
"progress": 35,
"stage": "geometry",
"vehicle": {
"id": "veh_78qtwrgh37bkr",
"make": "porsche",
"model": "911",
"year": 2024
},
"color": "red",
"generator": "meshy-7.1",
"files": null,
"polycount": null,
"error": null,
"estimated_seconds_remaining": 170,
"webhook": null,
"public": {
"id": "m3d_7f2k9q4hxn2b5c8d",
"url": "https://carimage.dev/api/v1/3d/public/m3d_7f2k9q4hxn2b5c8d",
"files": null,
"embed": {
"script": "https://carimage.dev/embed/3d.js",
"html": "<script src=\"https://carimage.dev/embed/3d.js\" async></script>\n<car-3d model=\"m3d_7f2k9q4hxn2b5c8d\"></car-3d>"
},
"published_at": "2026-09-16T09:20:00.000Z"
},
"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": 100,
"credits_remaining": 3870,
"already_owned": false
},
"request_id": "req_01j9x…"
}id is your request, scoped to your account; the shared model behind it is never exposed. When another customer already asked for this vehicle in this color, the answer is 200 with status: "ready" and the files straight away, still charged; when you already own it, nothing is charged (see Own it once). A Location header points at /api/v1/3d/{id} either way.
Own it once
You pay for a vehicle in a color once. Ordering the same vehicle in the same color again, from the same account, creates a new request for free: billing.credits_charged is 0 and billing.already_owned is true. That holds however you spell the vehicle (a make, model and year, or its id), whether the model is ready or still being made, and even after we rebuild models with a better recipe: your next order gets the new one, free. Only a live model counts: a failed one was refunded and is not yours, so asking again makes a fresh, charged attempt.
{
"data": {
"id": "8d2f4b16-93aa-4e0c-b1f7-5a0c2e7d9e41",
"object": "3d_model",
"status": "ready",
"progress": 100,
"stage": "done",
"vehicle": {
"id": "veh_78qtwrgh37bkr",
"make": "porsche",
"model": "911",
"year": 2024
},
"color": "red",
"generator": "meshy-7.1",
"files": {
"glb": {
"url": "https://carimage.dev/api/v1/3d/0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10/files/glb",
"bytes": 122821612,
"content_type": "model/gltf-binary"
},
"glb_web": {
"url": "https://carimage.dev/api/v1/3d/0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10/files/glb_web",
"bytes": 55821836,
"content_type": "model/gltf-binary"
},
"usdz": {
"url": "https://carimage.dev/api/v1/3d/0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10/files/usdz",
"bytes": 41643594,
"content_type": "model/vnd.usdz+zip"
},
"fbx": {
"url": "https://carimage.dev/api/v1/3d/0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10/files/fbx",
"bytes": 40446700,
"content_type": "application/octet-stream"
},
"thumbnail": {
"url": "https://carimage.dev/api/v1/3d/0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10/files/thumbnail",
"bytes": 111070,
"content_type": "image/png"
}
},
"polycount": 3215982,
"error": null,
"estimated_seconds_remaining": null,
"webhook": null,
"public": null,
"created_at": "2026-09-17T08:02:11.000Z",
"updated_at": "2026-09-16T09:17:41.000Z",
"ready_at": "2026-09-16T09:17:41.000Z"
},
"billing": {
"charged_on": "creation",
"credits_charged": 0,
"credits_remaining": 3870,
"already_owned": true
},
"request_id": "req_01j9z…"
}Every request is its own record with its own webhook and its own publish state, so a second order is how you get a fresh id to publish separately, or a webhook for a model you first asked for without one. Once a model is ready, re-downloading its files through GET /api/v1/3d/{id}/files/{kind} is free and needs no new order at all.
Status lifecycle
| status | Meaning | Then |
|---|---|---|
| queued | Accepted and charged; the source views are being rendered (stage views) or the provider has not picked the task up yet. | processing |
| processing | The mesh is being built and finished (stage geometry) or the color composed (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 10–20 minutes: its source views are rendered (stage views; the model waits until every one of them exists), the mesh is built from them once and finished (geometry) and the color asked for is composed onto it (texture). Every further color of that vehicle reuses the finished mesh and changes only the paint, 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_78qtwrgh37bkr",
"make": "porsche",
"model": "911",
"year": 2024
},
"color": "red",
"generator": "meshy-7.1",
"files": {
"glb": {
"url": "https://carimage.dev/api/v1/3d/0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10/files/glb",
"bytes": 122821612,
"content_type": "model/gltf-binary"
},
"glb_web": {
"url": "https://carimage.dev/api/v1/3d/0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10/files/glb_web",
"bytes": 55821836,
"content_type": "model/gltf-binary"
},
"usdz": {
"url": "https://carimage.dev/api/v1/3d/0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10/files/usdz",
"bytes": 41643594,
"content_type": "model/vnd.usdz+zip"
},
"fbx": {
"url": "https://carimage.dev/api/v1/3d/0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10/files/fbx",
"bytes": 40446700,
"content_type": "application/octet-stream"
},
"thumbnail": {
"url": "https://carimage.dev/api/v1/3d/0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10/files/thumbnail",
"bytes": 111070,
"content_type": "image/png"
}
},
"polycount": 3215982,
"error": null,
"estimated_seconds_remaining": null,
"webhook": null,
"public": null,
"created_at": "2026-09-16T09:12:04.000Z",
"updated_at": "2026-09-16T09:17:41.000Z",
"ready_at": "2026-09-16T09:17:41.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 120 MB | The original: Blender, Unity, Unreal, three.js, game engines, further processing. |
glb_web | model/gltf-binary | about 45–60 MB | The same model rebuilt for browsers (meshopt-compressed, WebP textures at full size). What a published model serves; use it for any web viewer. |
usdz | model/vnd.usdz+zip | about 40 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, glb_web, usdz, fbx, thumbnail is a 404. To show the model on a page without any of this, publish it.
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 porsche-911-2024-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_78qtwrgh37bkr",
"make": "porsche",
"model": "911",
"year": 2024
},
"color": "red",
"generator": "meshy-7.1",
"files": {
"glb": {
"url": "https://carimage.dev/api/v1/3d/0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10/files/glb",
"bytes": 122821612,
"content_type": "model/gltf-binary"
},
"glb_web": {
"url": "https://carimage.dev/api/v1/3d/0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10/files/glb_web",
"bytes": 55821836,
"content_type": "model/gltf-binary"
},
"usdz": {
"url": "https://carimage.dev/api/v1/3d/0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10/files/usdz",
"bytes": 41643594,
"content_type": "model/vnd.usdz+zip"
},
"fbx": {
"url": "https://carimage.dev/api/v1/3d/0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10/files/fbx",
"bytes": 40446700,
"content_type": "application/octet-stream"
},
"thumbnail": {
"url": "https://carimage.dev/api/v1/3d/0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10/files/thumbnail",
"bytes": 111070,
"content_type": "image/png"
}
},
"polycount": 3215982,
"error": null,
"estimated_seconds_remaining": null,
"webhook": null,
"public": null,
"created_at": "2026-09-16T09:12:04.000Z",
"updated_at": "2026-09-16T09:17:41.000Z",
"ready_at": "2026-09-16T09:17:41.000Z"
},
"sent_at": "2026-09-16T09:17:42.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
}Host it anywhere
Publishing a model gives it key-free URLs on our CDN and a two-line embed, so a product page, a listing or a configurator shows it without you hosting a file or putting a key anywhere near a browser. Publish at creation with publish: true, or any time later:
/api/v1/3d/{id}/publishcurl --fail-with-body -X POST \
-H "Authorization: Bearer $CAR_IMAGE_API_KEY" \
https://carimage.dev/api/v1/3d/0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10/publish
# -> data.public.id, data.public.files.glb.url, data.public.embed.html
# Take it down again (the links stop working within the hour):
curl -X DELETE -H "Authorization: Bearer $CAR_IMAGE_API_KEY" https://carimage.dev/api/v1/3d/0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10/publish{
"data": {
"id": "0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10",
"object": "3d_model",
"status": "ready",
"progress": 100,
"stage": "done",
"vehicle": {
"id": "veh_78qtwrgh37bkr",
"make": "porsche",
"model": "911",
"year": 2024
},
"color": "red",
"generator": "meshy-7.1",
"files": {
"glb": {
"url": "https://carimage.dev/api/v1/3d/0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10/files/glb",
"bytes": 122821612,
"content_type": "model/gltf-binary"
},
"glb_web": {
"url": "https://carimage.dev/api/v1/3d/0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10/files/glb_web",
"bytes": 55821836,
"content_type": "model/gltf-binary"
},
"usdz": {
"url": "https://carimage.dev/api/v1/3d/0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10/files/usdz",
"bytes": 41643594,
"content_type": "model/vnd.usdz+zip"
},
"fbx": {
"url": "https://carimage.dev/api/v1/3d/0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10/files/fbx",
"bytes": 40446700,
"content_type": "application/octet-stream"
},
"thumbnail": {
"url": "https://carimage.dev/api/v1/3d/0c5e0b2a-7f4a-4c8e-9d3b-2f1e6a9c4d10/files/thumbnail",
"bytes": 111070,
"content_type": "image/png"
}
},
"polycount": 3215982,
"error": null,
"estimated_seconds_remaining": null,
"webhook": null,
"public": {
"id": "m3d_7f2k9q4hxn2b5c8d",
"url": "https://carimage.dev/api/v1/3d/public/m3d_7f2k9q4hxn2b5c8d",
"files": {
"glb": {
"url": "https://carimage.dev/api/v1/3d/public/m3d_7f2k9q4hxn2b5c8d/model.glb",
"bytes": 55821836,
"content_type": "model/gltf-binary"
},
"usdz": {
"url": "https://carimage.dev/api/v1/3d/public/m3d_7f2k9q4hxn2b5c8d/model.usdz",
"bytes": 41643594,
"content_type": "model/vnd.usdz+zip"
},
"poster": {
"url": "https://carimage.dev/api/v1/3d/public/m3d_7f2k9q4hxn2b5c8d/poster.png",
"bytes": 111070,
"content_type": "image/png"
}
},
"embed": {
"script": "https://carimage.dev/embed/3d.js",
"html": "<script src=\"https://carimage.dev/embed/3d.js\" async></script>\n<car-3d model=\"m3d_7f2k9q4hxn2b5c8d\"></car-3d>"
},
"published_at": "2026-09-16T09:20:00.000Z"
},
"created_at": "2026-09-16T09:12:04.000Z",
"updated_at": "2026-09-16T09:17:41.000Z",
"ready_at": "2026-09-16T09:17:41.000Z"
},
"request_id": "req_01j9y…"
}public.id is unguessable and stays the same for as long as the model is published; publishing again is a no-op. You can publish before the model is ready: the URLs are stable from that moment, public.files is null until the files exist, and the embed shows a progress note in the meantime. Publishing is free and so is every load.
The embed
Paste public.embed.html into any HTML. The script is loaded once per page (it is safe to include on every page); each <car-3d> shows one model. It shows the poster at once, fetches Google's <model-viewer> the first time a model scrolls into view, and never sees an API key. This one runs on the demo model above, behind a button because the file is about 29.3 MB:

<script src="https://carimage.dev/embed/3d.js" async></script>
<car-3d model="m3d_7f2k9q4hxn2b5c8d" view="front-3-4" spin backdrop="studio" ar></car-3d>| Attribute | Values | Effect |
|---|---|---|
model | m3d_… | The public id from publishing. The only required attribute. |
view | front-3-4 (default) · front · side · rear-3-4 · rear · rear-3-4-right · side-right · front-3-4-right · top | The opening camera angle: the image views' nine angles, the eight around the car at the same yaw and top from straight above. |
spin | present, or degrees per second (default 30) | Auto-rotate until the visitor takes the camera. |
backdrop | none (default) · light · dark · studio · any CSS background | Behind the model. Presets also tune exposure and shadow; a custom value is pure CSS and never reaches the API. |
ar | present | An AR button on phones: Scene Viewer and WebXR on Android, Quick Look via the USDZ on iOS. |
static | present | No camera controls, no spin: a still that still loads the mesh. |
no-zoom | present | Leave the wheel and pinch to the page. |
alt | text | Alt text for the poster and the viewer; defaults to the model's own. |
src, ios-src, poster | URLs | Files you host yourself instead of a public id, for the same element. |
Any other attribute (exposure, camera-orbit, shadow-intensity, environment-image, …) is passed to <model-viewer> as is, so its whole vocabulary is available. Size the element with CSS: it is display: block, 100% wide and 4:3 by default. It fires car-3d:load once the mesh is on screen and car-3d:error when it cannot be, and element.data holds the public JSON.
<car-3d model="m3d_7f2k9q4hxn2b5c8d" backdrop="dark"></car-3d>
<car-3d model="m3d_7f2k9q4hxn2b5c8d" backdrop="#0f1117"></car-3d>
<car-3d model="m3d_7f2k9q4hxn2b5c8d" backdrop="radial-gradient(120% 120% at 50% 30%, #1b2233, #0a0c13)"></car-3d>
<car-3d model="m3d_7f2k9q4hxn2b5c8d" backdrop="url(/img/showroom.jpg) center/cover" static></car-3d>Without the script
public.files are ordinary URLs: model.glb (the browser build), model.usdz and poster.png, each a 302 to an immutable copy on the CDN, cacheable and readable from any origin. Point any viewer at them, <model-viewer> included (public.embed.model_viewer on the public JSON is this snippet, filled in):
<script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/4.1.0/model-viewer.min.js"></script>
<model-viewer
src="https://carimage.dev/api/v1/3d/public/m3d_7f2k9q4hxn2b5c8d/model.glb"
ios-src="https://carimage.dev/api/v1/3d/public/m3d_7f2k9q4hxn2b5c8d/model.usdz"
poster="https://carimage.dev/api/v1/3d/public/m3d_7f2k9q4hxn2b5c8d/poster.png"
alt="2024 Porsche 911 in red"
camera-controls auto-rotate ar
camera-orbit="35deg 78deg auto"
shadow-intensity="1"
style="width: 100%; aspect-ratio: 4 / 3"
></model-viewer>/api/v1/3d/public/{public_id}What the embed script reads, and what you can read from a browser too: status and progress, the vehicle's name, the paint and its swatch, alt text, the file URLs once ready and both snippets. Nothing about your account is in it.
{
"data": {
"id": "m3d_7f2k9q4hxn2b5c8d",
"object": "3d_model_public",
"status": "ready",
"progress": 100,
"estimated_seconds_remaining": null,
"vehicle": {
"id": "veh_78qtwrgh37bkr",
"make": "porsche",
"model": "911",
"year": 2024,
"name": "2024 Porsche 911"
},
"color": "red",
"color_hex": "#b3151b",
"alt": "3D model of a 2024 Porsche 911 in red",
"files": {
"glb": {
"url": "https://carimage.dev/api/v1/3d/public/m3d_7f2k9q4hxn2b5c8d/model.glb",
"bytes": 55821836,
"content_type": "model/gltf-binary"
},
"usdz": {
"url": "https://carimage.dev/api/v1/3d/public/m3d_7f2k9q4hxn2b5c8d/model.usdz",
"bytes": 41643594,
"content_type": "model/vnd.usdz+zip"
},
"poster": {
"url": "https://carimage.dev/api/v1/3d/public/m3d_7f2k9q4hxn2b5c8d/poster.png",
"bytes": 111070,
"content_type": "image/png"
}
},
"embed": {
"script": "https://carimage.dev/embed/3d.js",
"html": "<script src=\"https://carimage.dev/embed/3d.js\" async></script>\n<car-3d model=\"m3d_7f2k9q4hxn2b5c8d\"></car-3d>",
"model_viewer": "<model-viewer …></model-viewer>"
},
"published_at": "2026-09-16T09:20:00.000Z",
"updated_at": "2026-09-16T09:20:00.000Z"
},
"request_id": "req_01j9y…"
}/api/v1/3d/{id}/publishUnpublishing deletes the copies and forgets the id: the URLs and the embed stop working within the hour, once edge caches expire, and public is null again. Publishing later gets a new id. The <car-3d> element keeps showing the poster it already has and says the model is no longer published.
files need your API key and redirect to links that expire after an hour; neither belongs in HTML. A published model's URLs need nothing and never expire while it is published. Publish, or download and host the files yourself; never proxy the key.Pricing
- 100 credits per vehicle and color, 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. - Free once you own it: ordering a vehicle and color your account already holds a live request for charges nothing (
already_owned: true). - Polling, listing, downloads, webhook deliveries, publishing and every load of a published model are free.
- A model that fails is refunded in full; ask again and a fresh attempt is made.
402when the balance is short of 100 credits (withbalance,required_creditsand"code": "subscription_required_for_credits"); nothing is charged. Credits are not sold: Pro includes 1,000 a month and Business 4,000, so the fix is a plan (checkout_url), never a retry.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, apublishthat is not a boolean, awebhook_urlthat is not public HTTPS, or awebhook_secretwithout a URL.402— fewer than 100 credits (subscription_required_for_credits). Do not retry; tell the human, with the plan link the problem carries.404— the vehicle is not in the catalog, no vehicle has that id, no request of yours has that model id, or no published model has that public id.409— a file was asked for before the model was ready (Retry-After), a failed model was asked to publish, or anIdempotency-Keyretry overtook the first request.503— at capacity or disabled; waitretry_after_seconds.
Every failure is application/problem+json. See Errors.