3dassets.dev

REST API v1

Stable JSON over HTTPS. Reads are public and cached; writes need a Bearer API key.

Base URL https://3dassets.dev/api/v1. OpenAPI: /api/v1/openapi.json. Errors always look like { "error": { "code", "message", "details?" } }.

Read

bash
# search
curl "https://3dassets.dev/api/v1/assets?q=bench&category=props&theme=fantasy&style=low-poly&sort=popular"

# one asset (includes cdnUrl, downloadUrl, license.attributionText, stats, usage snippets)
curl "https://3dassets.dev/api/v1/assets/park-bench"

# same thing as Markdown
curl "https://3dassets.dev/assets/park-bench.md"

# taxonomy
curl "https://3dassets.dev/api/v1/categories"   # also /tags /licenses /packs

Filters on GET /assets: q, category, tag, theme, style, contributor, featured=true, sort=newest|popular|title, page, limit (≤100).

Accounts

bash
# 1. create (a 6-digit code is emailed)
curl -X POST https://3dassets.dev/api/v1/accounts -H 'content-type: application/json' \
  -d '{"email":"[email protected]","name":"Ada","username":"ada","source":"api"}'

# 2. verify → API key (shown once)
curl -X POST https://3dassets.dev/api/v1/accounts/verify -H 'content-type: application/json' \
  -d '{"email":"[email protected]","code":"123456"}'

# 3. check
curl https://3dassets.dev/api/v1/me -H 'Authorization: Bearer <apiKey>'

Accounts have no passwords

Signing up needs a username and an email, nothing else. Everything after that is a six-digit code we email, so there is never a credential for a user to paste into an agent session.

/accounts/login-code answers the same 200 whether or not the address has an account, so it cannot be used to discover who is registered.

Submit an asset

Two routes to the same place. Both end in processing (validation, re-encoding) and then a human review. Poll GET /assets/{slug} with your key to watch status move through processing → pending → published (or failed / rejected with a reason).

CC0 1.0 Universal is applied automatically. Set attestation: true only after the contributor accepts the irrevocable CC0 dedication and confirms their publishing rights. No licence or tags are needed in the request. If supplied, license must be cc0-1.0; other values receive a validation error. Missing or false acceptance is rejected. An API key alone does not grant permission to dedicate a model.

From a URL (simplest)

bash
curl -X POST https://3dassets.dev/api/v1/assets/from-url -H 'Authorization: Bearer <apiKey>' -H 'content-type: application/json' -d '{
  "url": "https://example.com/models/bench.glb",
  "title": "Park bench",
  "summary": "Low-poly wooden park bench, 1.8 m wide, PBR textures.",
  "category": "props",
  "attestation": true
}'

Direct upload

bash
# presign
curl -X POST https://3dassets.dev/api/v1/uploads/presign -H 'Authorization: Bearer <apiKey>' -H 'content-type: application/json' \
  -d '{"filename":"bench.glb","contentLength":1234567,"withPreview":false}'
# → { upload: { url, key, method: "PUT", headers } }

# upload the bytes straight to storage
curl -X PUT "<upload.url>" -H 'content-type: model/gltf-binary' --data-binary @bench.glb

# create the asset
curl -X POST https://3dassets.dev/api/v1/assets -H 'Authorization: Bearer <apiKey>' -H 'content-type: application/json' \
  -d '{"quarantineKey":"<upload.key>","title":"Park bench","summary":"…","category":"props","attestation":true}'

Edit or withdraw your own asset

PATCH /assets/{slug} changes the metadata of an asset you submitted: title, summary, description, category, tags, sourceUrl, aiGenerated, aiModel. The uploaded GLB is immutable: to change the model itself, submit a new asset.

GET /me/assets reports pendingEdit for anything you have waiting. DELETE /assets/{slug} withdraws the asset itself, permanently. Someone else’s asset answers 404 to all of these.

bash
curl -X PATCH https://3dassets.dev/api/v1/assets/park-bench -H 'Authorization: Bearer <apiKey>' -H 'content-type: application/json' \
  -d '{"summary":"Low-poly wooden park bench, 1.8 m wide, 2k PBR textures."}'

Rules every file must pass

Rejections come back with a machine-readable code (EXTERNAL_URI, LIMIT_EXCEEDED, VALIDATION_FAILED, …) and a human sentence you can show as-is.

Rate limits

Everything here is free, and stays free because nobody gets to take the whole catalogue at once. Requests are counted in fixed windows against your API key, or against your IP address when you send none, so a key is worth having even for read-only work.

WhatAnonymous (per IP)With an API key
Search & read
GET /api/v1/*, /assets/{slug}.md, MCP read tools
60/min · 1,000/hour120/min · 3,000/hour
Downloads
/download/{slug}
30/min · 300/hour · 1,000/day60/min · 600/hour · 3,000/day
Submissions & edits
POST /api/v1/assets, /assets/from-url, /uploads/presign, /packs/submissions, /reports, MCP write tools
5/min · 30/hour · 100/day20/min · 150/hour · 400/day
Accounts
POST /api/v1/accounts/verify, /accounts/session, key rotation
10/hour · 30/day10/hour · 30/day
Account emails
POST /api/v1/accounts, /accounts/login-code, /accounts/resend
6/hour · 20/day6/hour · 20/day

Account calls are counted per IP address whether or not you send a key. Reads and downloads answer with RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (seconds); every 429 carries those plus Retry-After and the usual error body:

json
{ "error": { "code": "RATE_LIMITED", "message": "Rate limit exceeded for downloads (…). Retry in 42s.",
  "details": { "bucket": "download", "retryAfterSeconds": 42, "limit": 30, "window": "minute" } } }

Other

AI model attribution

Set aiGenerated: true if AI helped create geometry, generating code or textures, and optionally include aiModel (up to 160 characters). Use GET /api/v1/ai-models?q=... or MCP list_ai_models for OpenRouter suggestions refreshed hourly. Custom model and 3D-tool names are accepted. Leave unknown models unspecified; never guess. Using an agent only to upload does not count as AI creation.

The asset info panel, JSON and Markdown show this as contributor-reported attribution. A model catalogue outage does not block uploading. Switching AI use off clears the model name.