3D Valley API
Search a free CC0 library of 3D models, PBR textures and HDRIs, and generate, retexture, remesh, repair, reduce, compress and convert 3D models, from code or from an AI agent. One REST API, one MCP server, the same credits as the website.
- REST:
https://3dvalley.com/api/v1 - MCP:
https://3dvalley.com/mcp(streamable HTTP) - OpenAPI 3.1: /api/v1/openapi.json
- This page as Markdown: /docs/api.md, errors: /docs/api/errors
MCP setup
Setup for Claude, ChatGPT, Cursor, VS Code and Windsurf, step by step: MCP server.
Claude Code:
claude mcp add --transport http 3dvalley https://3dvalley.com/mcp
Cursor, Windsurf, VS Code and other clients: add a remote MCP server with the URL https://3dvalley.com/mcp. The first time a tool needs your account, the client opens a sign-in page in your browser; approve it and you are connected. Searching the library works without signing in.
Tools: search_assets, get_asset, generate_model, edit_model, generate_texture, get_job, upload_file, account. A generation tool waits about 45 seconds, then hands back a job_id; get_job waits for it and never charges again. Calling a generation tool again with the same arguments resumes the same job.
Quick start
Search the library (no key needed):
curl "https://3dvalley.com/api/v1/assets?q=wooden+barrel&type=model&limit=5"
Make a key at /account/api, then generate a model from a picture and wait up to 50 seconds for it:
curl https://3dvalley.com/api/v1/jobs \
-H "Authorization: Bearer 3dv_live_..." \
-H "Idempotency-Key: $(uuidgen)" \
-H "Prefer: wait=50" \
-H "Content-Type: application/json" \
-d '{"tool":"image-to-3d","model":"trellis-2","input":{"image":"https://example.com/chest.png"},"max_credits":20}'
The answer is 201 with the finished job, or 202 with Retry-After while it runs. Read it again with GET /api/v1/jobs/{id} (it also takes Prefer: wait=N).
Keys, credits and limits
Send keys as Authorization: Bearer 3dv_live_.... Each key has a name, scopes (library, tools) and a daily spending cap; keys are made, capped and revoked at /account/api. Only a hash of a key is stored.
Credits are the website's credits: one balance, 1 credit = $0.01, bought at /pricing. The library is free. Tools that use a model cost credits; repair, reduce, compress, convert, normal map and the QuadriFlow remesher are free.
| No key | Free key | Account that has bought credits | |
|---|---|---|---|
| Library | 60 requests/hour per IP | 1,000/hour, 10,000/day | 5,000/hour |
| Tools | no | yes, with credits | yes |
| Jobs at once | none | 2 | 5 |
| Daily spend per key | none | 100 credits (adjustable) | 1,000 credits (adjustable) |
Every counted answer, a 429 included, carries RateLimit-Policy, RateLimit and X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset. A 429 names the limit it hit in error.limit: rate, concurrency or spend_cap. Following a download link never counts.
Conventions
- Ids: library assets
ph:wooden_barrel_01,acg:Bricks076C; your generated models3dv:...; jobsjob_...; uploadsupl_.... - Lists take
limit(default 20, at most 100) andcursor, and answernext_cursor. detail=concise|fullon asset lists; concise by default.- An unknown parameter is a
400that lists the valid ones. Nothing is ignored silently. - Errors:
{ "error": { "code", "message", "fix", "docs" } }. See errors. - Versioning:
v1in the path; within v1 fields are only added.
Library
Attribution
The files are CC0: whoever downloads one may use it anywhere, with no credit. Using the API is a service, and it asks one thing back: an app or a website that shows library results from this API to its own users (a browser, a picker, a plugin) says "3D Valley" with a link to https://3dvalley.com near them. A game or a render made with the assets needs nothing. Every asset carries a ready credit line (credit, and credit_html with links) for anyone who wants to give one; agents writing a project's credits or README are asked to include it.
GET /assets
Search by meaning and keyword, with filters on what was measured from the files.
| Parameter | |
|---|---|
q | Words: wooden barrel, mossy rock |
type | model, texture, hdri |
category | A category slug; see GET /categories |
style | realistic, stylized, lowpoly, scan |
max_tris, min_tris | Triangle count |
min_size_m, max_size_m | Largest side in metres (models). Wall-sized pieces: min_size_m=2 |
maps | Must have all of these: base_color,normal,roughness,metallic,ao,height,displacement |
min_res | Smallest texture resolution: 1k or 2k (files are hosted at 1K and 2K) |
format | glb (models), zip (textures), hdr, exr (HDRIs) |
rigged, animated | true or false |
source | polyhaven, ambientcg, kenney, kaykit, cgbookcase, texturecan, openhdri |
pack | A kit's id from a result (kn:city-kit-roads): only that kit's pieces, which are made to fit together |
sort | relevance (default), popular, newest |
similar_to | An asset id: the nearest by meaning |
{ "results": [{ "id": "ph:wooden_barrel_01", "name": "Wooden Barrel 01", "type": "model",
"category": "household", "style": "realistic", "tags": ["barrel","wood","medieval"],
"license": "CC0-1.0", "size_m": [0.62, 0.62, 0.91], "tris": 4820,
"maps": ["base_color","normal","roughness","metallic","ao"], "max_res": "2k",
"preview": "https://3dvalley.com/api/v1/assets/ph:wooden_barrel_01/preview?size=512",
"download": "https://3dvalley.com/api/v1/assets/ph:wooden_barrel_01/download?format=glb" }],
"total": 37, "next_cursor": "c_..." }
Every mirrored model is a GLB in metres, +Y up, with its origin at the base of its footprint, so it stands on the floor where an engine drops it. size_m is its bounding box.
GET /assets/{id}
Everything above, plus every file (format, resolution, bytes, md5, URL), up_axis, origin, normal_convention, author, source URL, a credit line (credit, credit_html), related assets, and import notes for Godot, Unity (glTFast), three.js (GLTFLoader) and Blender.
GET /assets/{id}/download
?format=glb for models (convert to OBJ or STL with the convert tool), zip for textures (Poly Haven's single maps also with map=normal and the like), hdr|exr for HDRIs, with res=1k|2k; a larger res gives the largest there is. Answers 302 to our CDN and counts the download. An asset not mirrored yet answers 404 with its source_url.
GET /assets/{id}/preview
?size=256|512|1024. A 302 to the preview picture.
GET /categories
?type=model: every category with its count.
Tools
GET /tools
The price list: every tool with its models, credits, typical seconds, inputs, options, outputs and limits. Read it before spending.
| Tool | Input | Options | Output |
|---|---|---|---|
image-to-3d | image | model, seed | glb |
text-to-3d | prompt | model, image_model, seed | glb, image |
retexture | mesh + image or prompt | model, image_model, seed | glb |
remesh | mesh | model, faces, topology (quads/triangles) | glb |
texture | prompt | model, seed | base_color, normal, height, roughness, metallic |
normal-map | image | strength, directx | normal |
repair | mesh | mode (patch/solid), format (glb/stl) | glb or stl |
reduce | mesh | faces or ratio | glb |
compress | mesh | textures (webp/png_jpeg/keep), max_res, meshopt | glb |
convert | mesh | to (glb/obj/stl) | the file |
An input is any of: a public http(s) URL, a data: URI (pictures), an upload id upl_..., a library id ph:wooden_barrel_01 (mirrored assets), or an earlier job (job_... or its asset id 3dv:...). So generate, remesh and compress chain without downloading in between.
POST /jobs
POST /api/v1/jobs
Authorization: Bearer 3dv_live_...
Idempotency-Key: 2b7e... (required)
Prefer: wait=50 (optional, at most 50 s)
{ "tool": "image-to-3d", "model": "trellis-2-pro",
"input": { "image": "https://example.com/chest.png" },
"options": { "seed": 7 }, "max_credits": 20, "publish": false,
"webhook_url": "https://example.com/hooks/3dv" }
modelis optional: the default is the model the website's tool preselects.max_creditsrefuses the job if it costs more.- The same
Idempotency-Keywith the same body returns the same job and charges nothing new; with another body it is a422; while the first request is still being processed, a409. Keys last 24 hours. Agents retry; this makes retrying safe. - Credits are taken when a stage starts and given back if it fails. Text to 3D is two stages (the picture, then the model), each charged when it starts.
{ "id": "job_8Hq2...", "object": "job", "status": "succeeded", "tool": "image-to-3d",
"model": "trellis-2-pro", "progress": 1,
"credits": { "price": 15, "charged": 15, "balance": 1485 },
"output": {
"glb": { "url": "https://files.3dvalley.com/...", "bytes": 8123456, "content_type": "model/gltf-binary", "expires_at": "..." },
"stats": { "tris": 198000, "size_m": [0.6, 0.6, 0.9], "up_axis": "+Y", "origin": "other",
"maps": ["base_color","normal","roughness","metallic"], "max_res": 4096 } },
"asset_id": "3dv:8Hq2...", "error": null, "created_at": "...", "finished_at": "...", "expires_at": "..." }
Statuses: queued, running, succeeded, failed, canceled. Outputs are kept 30 days; links in a job are signed for 24 hours and signed afresh on every read.
GET /jobs/{id}
The job as it stands. Takes Prefer: wait=N. Never charges.
GET /jobs/{id}/events
Server-sent events: progress while it runs, then one done with the job.
POST /jobs/{id}/cancel
A job that has not started is canceled. A running one answers 409: no model can be stopped once it is working, and a failed run gives its credits back anyway.
GET /jobs
?status=&tool=&cursor=&limit=. Your jobs, newest first. API runs show in the website's History tab too, marked "Via: API".
Uploads
POST /api/v1/uploads
{ "filename": "chest.glb", "bytes": 812345 }
Answers { "id": "upl_...", "put_url": "...", "curl": "curl -X PUT --data-binary @chest.glb \"...\"" }. Send the file with that plain PUT (no auth header; the URL is signed for an hour), then use the id as an input for 24 hours. Up to 60 MB.
Webhooks
Pass webhook_url (https) with a job. When it ends you receive POST { "type": "job.succeeded" | "job.failed" | "job.canceled", "timestamp", "data": <the job> }, signed in the Standard Webhooks format: headers webhook-id, webhook-timestamp, webhook-signature (v1, and the base64 HMAC-SHA256 of {id}.{timestamp}.{body} under the key's signing secret, shown at /account/api). Failed deliveries are retried after 30 s, 2 min, 10 min, 1 h and 6 h. The webhook-id is the same on every retry: dedupe on it.
Account
GET /account: balance, its expiry, tier, limits, this key's daily cap and what is left of it, and the prices.GET /account/usage?days=30: credits and jobs per day through the API.
OAuth
For MCP clients. Discovery: /.well-known/oauth-protected-resource and /.well-known/oauth-authorization-server. Authorization code with PKCE (S256); clients identify with a Client ID Metadata Document (an https client_id) or register at /api/oauth/register. The access token is an API key with the scopes the user approved (library by default; tools when a tool would spend credits, asked for with a 403 insufficient_scope). It appears at /account/api and can be revoked there.