{"id":70978,"plugin_id":"plugin_asdk_app_6aa6d30bc5d88191a148b89fa75dd324","kind":"skill","collection_source":"plugin_package","comparison_source":null,"observed_at":"2026-10-29T19:09:04.635Z","digest":"5ef625ccfc1d7f78b6705e002389c4299187973b5541a1988ef1fc8e963774a8","against":null,"payload":{"description":"Get a textured 3D model (GLB, USDZ, FBX and a thumbnail) of any real vehicle in any paint color from the Car Image API, for AR views, configurators, game assets, product pages and 3D scenes, and put it on a web page with a two-line embed that Car Image hosts. Covers the order of work (look the vehicle up, confirm the price, create by vehicle id), what a model costs (100 credits, charged at creation, free once the account owns that vehicle in that color), how long it takes and how to wait in a conversation, polling versus webhooks, verifying the webhook signature, downloading the files, publishing a model and the <car-3d> element. Use when the user wants a 3D model, a GLB, a USDZ, an FBX, an AR-ready asset of a car, or a 3D car on a page; do not use for 2D renders (car-image), signed image URLs (car-image-urls) or finding which vehicle to model (vehicle-catalog).","included_files":[{"relative_path":"agents/openai.yaml","size_in_bytes":437}],"name":"car-3d","skill_md_contents":"---\nname: car-3d\ndescription: Get a textured 3D model (GLB, USDZ, FBX and a thumbnail) of any real vehicle in any paint color from the Car Image API, for AR views, configurators, game assets, product pages and 3D scenes, and put it on a web page with a two-line embed that Car Image hosts. Covers the order of work (look the vehicle up, confirm the price, create by vehicle id), what a model costs (100 credits, charged at creation, free once the account owns that vehicle in that color), how long it takes and how to wait in a conversation, polling versus webhooks, verifying the webhook signature, downloading the files, publishing a model and the <car-3d> element. Use when the user wants a 3D model, a GLB, a USDZ, an FBX, an AR-ready asset of a car, or a 3D car on a page; do not use for 2D renders (car-image), signed image URLs (car-image-urls) or finding which vehicle to model (vehicle-catalog).\n---\n\n# 3D models of real vehicles\n\n`POST /api/v1/3d` turns any catalog vehicle, in any paint, into a textured 3D model: **GLB, USDZ, FBX, a thumbnail and `glb_web`**, a smaller browser build of the GLB. Models are built from the same renders the image API serves, so a model matches the pictures the user already shows, and every color of a vehicle is one mesh with a different texture.\n\nWith MCP connected the tools are `create_3d_model`, `get_3d_model` and `publish_3d_model`; they are on `?toolset=all`, which the plugin configures, and a connection on the bare URL has the core seven only (the `car-image-mcp` skill). Over REST: `POST /api/v1/3d`, `GET /api/v1/3d/{id}`, `GET /api/v1/3d/{id}/files/{kind}`, `GET /api/v1/3d?limit=`, `POST|DELETE /api/v1/3d/{id}/publish`, and the key-free `GET /api/v1/3d/public/{public_id}`. The CLI has `car-image 3d create|get|download|list|publish`; the SDK has `create3dModel`, `get3dModel`, `list3dModels`, `download3dModel`, `publish3dModel` and `unpublish3dModel`. Full reference: [`/docs/3d`](https://carimage.dev/docs/3d?ref=plugin).\n\n## The order of work\n\n1. **Look the vehicle up** (free; the `vehicle-catalog` skill): `resolve_vehicle` for a name, `decode_vin` for a VIN. Keep the vehicle id. A model of the wrong car costs a hundred times an image of it, so this step is never skipped, and a lookup that is not `high` confidence in the year asked for goes back to the user first.\n2. **Say what it is and what it costs, and wait for a yes**: the year, make and model the lookup named, the paint, 100 credits ($1.00) unless the account already owns it, and ten to twenty minutes for a first model. A preview image of that vehicle in that paint (1 credit; the `car-image` skill shows one in the reply) is a cheap way to agree on both before the larger charge.\n3. **Create it by id**: `create_3d_model({ vehicle: \"veh_…\", color, publish: true })`. Keep the request `id`.\n4. **Wait without hammering**, then hand over the files or the embed.\n\n## What it costs — say this first\n\n- **100 credits ($1.00) per vehicle and color**, charged at creation whether the model is cached, in progress or new. Cache state never changes the price, exactly as with images.\n- **Free once owned.** An account that already holds a live request for a vehicle in a color owns it: ordering it again creates a new request for nothing (`billing.credits_charged: 0`, `billing.already_owned: true`), whatever spelling, whether the model is ready or still being made, and even after Car Image rebuilds models with a better recipe. So a re-order is never a second bill; only a failed model (refunded, not owned) makes the next attempt a charged one.\n- Polling, listing, downloads, webhook deliveries, **publishing and every load of a published model are free**.\n- A model that **fails is refunded** in full. A `503` at capacity charges nothing.\n- A request is a hundred times an image: **tell the user the price and confirm before calling `create_3d_model`** for a vehicle and color the account does not own yet, and call `get_account` first when the balance may be short. Never retry a `402`.\n\n## How long it takes\n\n| Case | Wall time | Why |\n| --- | --- | --- |\n| First model of a vehicle | **10–20 minutes** | The four source views are rendered and waited for (`stage: views`), the mesh is built and finished (`geometry`) and the color composed (`texture`). |\n| Another color of the same vehicle | **1–2 minutes** | The finished mesh already exists; only the paint changes. |\n| Someone already asked for this vehicle in this color | immediate | `200` with `status: \"ready\"` and the files; charged unless the account already owns it. |\n\nSay this before the user waits. Do not present a model as instant.\n\n## The request\n\n```json\n{ \"make\": \"Toyota\", \"model\": \"Camry\", \"year\": 2025, \"color\": \"red\", \"publish\": true }\n```\n\n- Name the vehicle with `make`, `model` and `year`, **or** with `vehicle`, a stable catalog id such as `veh_errc87t1jgata` (from `search_vehicles`, `resolve_vehicle` or `decode_vin`). Both together is a `400`; an unknown id is a `404`.\n- `color` is one of the 15 presets or any hex (`\"#1a2b3c\"` or `\"1a2b3c\"`); the response echoes `#1a2b3c`. Default `silver`.\n- `publish: true` publishes the model at once (below). Free. Pass it whenever the model is for a web page.\n- `webhook_url` (public HTTPS) and `webhook_secret` are optional; see below.\n- Send an `Idempotency-Key` header over REST so a retried request replays the first answer instead of charging again (the SDK and CLI do this for you).\n\nPrefer `vehicle`: the id comes from the lookup, so the model is of the car the user confirmed. A name typed from memory can answer `404`, or resolve to a neighbouring model, and a model of the wrong car costs 100 credits.\n\n## The response and its lifecycle\n\n`202 Accepted` while the model is queued or processing, `200` when it was already ready:\n\n```json\n{\n  \"data\": {\n    \"id\": \"0c5e0b2a-…\", \"object\": \"3d_model\",\n    \"status\": \"processing\", \"progress\": 35, \"stage\": \"geometry\",\n    \"vehicle\": { \"id\": \"veh_errc87t1jgata\", \"make\": \"toyota\", \"model\": \"camry\", \"year\": 2025 },\n    \"color\": \"red\", \"generator\": \"meshy-7.1\",\n    \"files\": null, \"polycount\": null, \"error\": null,\n    \"estimated_seconds_remaining\": 170, \"webhook\": null,\n    \"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\": \"…\" },\n    \"created_at\": \"…\", \"updated_at\": \"…\", \"ready_at\": null\n  },\n  \"billing\": { \"charged_on\": \"creation\", \"credits_charged\": 100, \"credits_remaining\": 3870, \"already_owned\": false },\n  \"request_id\": \"…\"\n}\n```\n\n`status` goes `queued` → `processing` → `ready` or `failed`. `progress` is 0–100; `estimated_seconds_remaining` is a rough guide, not a promise. When `ready`, `files` holds `glb`, `glb_web`, `usdz`, `fbx` and `thumbnail`, each `{url, bytes, content_type}`. When `failed`, `error` carries `{code, message}` and the credits were refunded; asking again makes a fresh attempt. `public` is `null` until the request is published.\n\n`id` is the user's request; the shared model behind it is never exposed. Every request is its own record with its own webhook and its own publish state.\n\n## Waiting: poll or webhook\n\n**Poll** `get_3d_model` / `GET /api/v1/3d/{id}` every **10–15 seconds**. The server re-reads the generator only when its last look is older than 15 seconds, so a tighter loop sees the same answer and wastes the rate limit. Stop on `ready` or `failed`. In a script, `car-image 3d get <id> --wait` and `car-image 3d create … --wait` do the waiting.\n\n**Webhook**: pass `webhook_url` and the API POSTs once when the model settles:\n\n- Body: `{\"event\": \"3d_model.ready\" | \"3d_model.failed\", \"data\": <the same JSON as GET>, \"sent_at\": \"…\"}`.\n- Headers: `X-CarImage-Event`, `X-CarImage-Delivery` (unique per attempt; ignore a duplicate) and, when `webhook_secret` was given, `X-CarImage-Signature: sha256=<hex HMAC-SHA256 of the raw body keyed with the secret>`.\n- The endpoint must answer any `2xx` within 10 seconds; a non-2xx or a timeout is retried with backoff up to five times.\n\nVerify the signature over the **raw bytes**, with a constant-time compare, before parsing:\n\n```ts\nimport { createHmac, timingSafeEqual } from \"node:crypto\";\n\n/** `secret` is the webhook_secret the model was requested with, from the app's own secret settings. */\nexport function signatureIsValid(rawBody: Buffer, given: string, secret: string): boolean {\n  const expected = \"sha256=\" + createHmac(\"sha256\", secret).update(rawBody).digest(\"hex\");\n  return given.length === expected.length && timingSafeEqual(Buffer.from(given), Buffer.from(expected));\n}\n\n// In the handler, before parsing the body:\nif (!signatureIsValid(rawBody, request.headers.get(\"x-carimage-signature\") ?? \"\", secret)) {\n  return new Response(\"bad signature\", { status: 401 });\n}\n```\n\nUse a webhook when the user's app has a public HTTPS endpoint; poll otherwise. Never send a `webhook_secret` anywhere but the request body.\n\n## In a conversation\n\nA chat has no page to embed into and no terminal to download with, so plan for what the user can open.\n\n- **Publish at creation** (`publish: true`). The `files` URLs need the account's credential on the first hop; the `public.files` URLs do not, so they are the links a person can click: `glb` to download or drop into any 3D viewer, `usdz` to open in AR on an iPhone or iPad, `poster` for a still. Say that anyone with a public link can load the model, and offer to unpublish when they are done.\n- **Do not hold the conversation for the wait.** After creating, say how long it should take (`estimated_seconds_remaining`), check `get_3d_model` once or twice at most while you have something else to do, and otherwise tell the user to ask for the model again later: the request id is all you need, and the wait costs nothing.\n- **When it is ready**, give the links with their sizes (`bytes`), say which to use for what, and show the still.\n- **When it fails**, the credits came back. Say so, and offer another attempt rather than starting one.\n\n## On a web page: publish it\n\nWhen the model is for a page — a listing, a product page, a configurator, a demo — do not download and host files. **Publish** it, with `publish: true` at creation or `publish_3d_model` (`POST /api/v1/3d/{id}/publish`, `car-image 3d publish <id>`, `client.publish3dModel(id)`) at any time, and paste `public.embed.html`:\n\n```html\n<script src=\"https://carimage.dev/embed/3d.js\" async></script>\n<car-3d model=\"m3d_7f2k9q4hxn2b5c8d\" view=\"front-3-4\" spin backdrop=\"studio\" ar></car-3d>\n```\n\n- The script goes on the page once; each `<car-3d>` shows one model. **No API key anywhere near the browser** — the element reads the model's public JSON and Car Image serves the files from its CDN. Free, every load.\n- It shows the poster at once, loads Google's `<model-viewer>` the first time it scrolls into view, and, if the model is still being made, shows a progress note and updates itself when it is ready — so you can publish at creation and ship the page before the model exists.\n- Attributes: `view` (`front-3-4` default, `front`, `side`, `rear-3-4`, `rear`, `rear-3-4-right`, `side-right`, `front-3-4-right`, `top`: the image views' eight angles plus a top view), `spin` (or a number of degrees per second), `backdrop` (`none` default, `light`, `dark`, `studio`, or any CSS background: `#0f1117`, a gradient, `url(…)`), `ar` (an AR button on phones), `static` (no controls), `no-zoom`, `alt`. Anything else (`exposure`, `camera-orbit`, `shadow-intensity`, …) is passed to `<model-viewer>`. Size it with CSS: it is `display: block`, 100% wide, 4:3 by default.\n- Pages that already load `<model-viewer>` can skip the script and use `public.files` directly: `model.glb` (the browser build), `model.usdz` (for `ios-src`) and `poster.png` are stable URLs that redirect to immutable copies on the CDN, readable from any origin. `GET /api/v1/3d/public/{public_id}` (no key) returns the same JSON the element reads, `embed.model_viewer` included.\n- `public.id` is unguessable, not secret: anyone holding the link can load the model. Unpublish (`unpublish: true`, `DELETE`, `--unpublish`) when it should not be: the links stop working within the hour, and a later publish gets a new id.\n\n## Downloading the files\n\nFor an app, a game engine, Blender or a pipeline, download instead. `GET /api/v1/3d/{id}/files/{glb|glb_web|usdz|fbx|thumbnail}` needs the API key and 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; `car-image 3d download <id> --format glb --out car.glb` and `client.download3dModel(id, \"glb\")` do the same. Before the model is ready it answers `409` with `Retry-After`.\n\n| File | Content type | Typical size | Use |\n| --- | --- | --- | --- |\n| `glb` | `model/gltf-binary` | about 100 MB | The original, unremeshed (about three million triangles, 4k textures): three.js, Blender, Unity, Unreal, further processing |\n| `glb_web` | `model/gltf-binary` | about 30 MB | The browser build (meshopt, WebP textures): any web viewer, when you host it yourself |\n| `usdz` | `model/vnd.usdz+zip` | about 25 MB | iOS AR Quick Look, visionOS; converted from a decimated build (about 300,000 triangles, the same 4k textures) |\n| `fbx` | `application/octet-stream` | similar to the GLB | Maya, 3ds Max, older pipelines |\n| `thumbnail` | `image/png` | a few hundred KB | A transparent still for posters and lists |\n\nNever put the API key, or the hour-long signed URL, in a page. Publishing exists so you never have to.\n\n## When something fails\n\n| Status | Meaning | Do |\n| --- | --- | --- |\n| 400 | Bad body: `vehicle` with make/model/year, an unparseable `color`, a `publish` that is not a boolean, a `webhook_url` that is not public HTTPS, a `webhook_secret` without a URL. | Fix and retry once. Nothing was charged. |\n| 402 | Fewer than 100 credits. | **Stop and ask the human** to top up. Never buy credits on your own. |\n| 404 | Vehicle not in the catalog under that name, unknown vehicle id, a request id that is not the user's, or an unpublished public id. Nothing was charged. | Look the vehicle up (`resolve_vehicle`; the problem's `suggestions` carry ids) and create by `vehicle`; check the id. |\n| 409 | A file was asked for before `ready`, a failed model was asked to publish, or an `Idempotency-Key` retry overtook the first request. | Wait `Retry-After`, then poll `get_3d_model`. |\n| 503 | 3D generation is at its daily capacity (`code: model_3d_at_capacity`, `retry_after_seconds`) or switched off. Nothing charged; images are unaffected. | Tell the user when it resumes; do not loop. |\n\nEvery failure is `application/problem+json` with a `request_id`; keep it. The full table is in the `car-image` skill's [references/errors.md](../car-image/references/errors.md).\n\n## Never do these on your own\n\n- Create a model of a vehicle you have not looked up, or one the user has not confirmed by name.\n- Create a model the user did not ask for, or a batch of them. Confirm the count and the cost (100 credits each, unless the account already owns that vehicle in that color) first.\n- Retry a `402`, or poll faster than every 10 seconds.\n- Put the API key, a `webhook_secret` or a signed file URL in a page, a repository or a log. Publish instead.\n- Publish a model the user wanted kept private, or leave one published after they asked for it to be taken down.\n- Claim the model reproduces a specific trim, options package or an individual listed vehicle: it is built from catalog-bounded studio renders, not a scan of a real car.\n"},"changes":[],"summary":"First saved snapshot. No earlier version is available for comparison.","summary_kind":"deterministic","summary_metadata":{}}