Car Image API
Documentation

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.

Interactive 3D model of a 2024 Porsche 911 made by the Car Image API

Choose a color. Only the paint changes.

Endpoints

POST/api/v1/3d
images:read100 credits, free once owned
GET/api/v1/3d/{id}
images:readfree
GET/api/v1/3d/{id}/files/{kind}
images:readfree
GET/api/v1/3d?limit=
images:readfree
POST/api/v1/3d/{id}/publish
images:readfree
DELETE/api/v1/3d/{id}/publish
images:readfree
GET/api/v1/3d/public/{public_id}
nonefree
GET/api/v1/3d/public/{public_id}/{file}
nonefree

The same operations are the create_3d_model, get_3d_model and publish_3d_model MCP tools (on ?toolset=all), the car-image 3d create|get|download|list|publish CLI commands and the SDK's create3dModel, get3dModel, list3dModels, download3dModel, publish3dModel and unpublish3dModel.

Create a model

POST/api/v1/3d
images:read100 credits, free once owned
POST /api/v1/3d body
ParameterTypeDescription
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.
HeaderExampleMeaning
Idempotency-Key3d-porsche-911-2024-redOptional 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.
bash
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}'
202 Accepted (200 when the model is already ready)
{
  "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.

200 OK — the same Porsche in red, ordered a second time
{
  "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

statusMeaningThen
queuedAccepted and charged; the source views are being rendered (stage views) or the provider has not picked the task up yet.processing
processingThe mesh is being built and finished (stage geometry) or the color composed (stage texture). progress climbs from 0 to 100.ready or failed
readyfiles holds every format; ready_at is set; the webhook, if any, has been queued.final
failederror 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

GET/api/v1/3d/{id}
images:readfree

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.

200 OK
{
  "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…"
}
GET/api/v1/3d?limit=
images:readfree

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

GET/api/v1/3d/{id}/files/{kind}
images:readfree
kindContent-TypeTypical sizeUse
glbmodel/gltf-binaryabout 120 MBThe original: Blender, Unity, Unreal, three.js, game engines, further processing.
glb_webmodel/gltf-binaryabout 45–60 MBThe same model rebuilt for browsers (meshopt-compressed, WebP textures at full size). What a published model serves; use it for any web viewer.
usdzmodel/vnd.usdz+zipabout 40 MBiOS AR Quick Look and visionOS; ios-src on <model-viewer>.
fbxapplication/octet-streamsimilar to the GLBMaya, 3ds Max and pipelines that still want FBX.
thumbnailimage/pnga few hundred KBA 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.

bash
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.glb

Webhooks

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:

POST <webhook_url>
{
  "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"
}
HeaderExampleMeaning
X-CarImage-Event3d_model.ready | 3d_model.failedThe event, also in the body.
X-CarImage-Delivery5c1d…Unique per delivery attempt; use it to ignore a duplicate.
X-CarImage-Signaturesha256=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:

app/api/car-image-webhook/route.ts
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:

POST/api/v1/3d/{id}/publish
images:readfree
bash
curl --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
200 OK — data.public
{
  "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:

Interactive 3D model of a 2024 Porsche 911 made by the Car Image API
html
<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>
AttributeValuesEffect
modelm3d_…The public id from publishing. The only required attribute.
viewfront-3-4 (default) · front · side · rear-3-4 · rear · rear-3-4-right · side-right · front-3-4-right · topThe opening camera angle: the image views' nine angles, the eight around the car at the same yaw and top from straight above.
spinpresent, or degrees per second (default 30)Auto-rotate until the visitor takes the camera.
backdropnone (default) · light · dark · studio · any CSS backgroundBehind the model. Presets also tune exposure and shadow; a custom value is pure CSS and never reaches the API.
arpresentAn AR button on phones: Scene Viewer and WebXR on Android, Quick Look via the USDZ on iOS.
staticpresentNo camera controls, no spin: a still that still loads the mesh.
no-zoompresentLeave the wheel and pinch to the page.
alttextAlt text for the poster and the viewer; defaults to the model's own.
src, ios-src, posterURLsFiles 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.

Backdrops: a preset, a brand color, a gradient, an image
<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):

html
<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>
GET/api/v1/3d/public/{public_id}
nonefree

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.

200 OK
{
  "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…"
}
DELETE/api/v1/3d/{id}/publish
images:readfree

Unpublishing 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.

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-Key makes 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.
  • 402 when the balance is short of 100 credits (with balance, required_credits and "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.
  • 503 with retry_after_seconds (and Retry-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, vehicle together with make, model and year, an unparseable color, a publish that is not a boolean, a webhook_url that is not public HTTPS, or a webhook_secret without 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 an Idempotency-Key retry overtook the first request.
  • 503 — at capacity or disabled; wait retry_after_seconds.

Every failure is application/problem+json. See Errors.