# 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](/api/v1/openapi.json)
- This page as Markdown: [/docs/api.md](/docs/api.md), errors: [/docs/api/errors](/docs/api/errors)

## MCP setup

Setup for Claude, ChatGPT, Cursor, VS Code and Windsurf, step by step: [MCP server](/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](/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](/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](/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 models `3dv:...`; jobs `job_...`; uploads `upl_...`.
- Lists take `limit` (default 20, at most 100) and `cursor`, and answer `next_cursor`.
- `detail=concise|full` on asset lists; concise by default.
- An unknown parameter is a `400` that lists the valid ones. Nothing is ignored silently.
- Errors: `{ "error": { "code", "message", "fix", "docs" } }`. See [errors](/docs/api/errors).
- Versioning: `v1` in 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" }
```

- `model` is optional: the default is the model the website's tool preselects.
- `max_credits` refuses the job if it costs more.
- The same `Idempotency-Key` with the same body returns the same job and charges nothing new; with another body it is a `422`; while the first request is still being processed, a `409`. 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](https://www.standardwebhooks.com) 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](/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](/account/api) and can be revoked there.
