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
# 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 /packsFilters on GET /assets: q, category, tag, theme, style, contributor, featured=true, sort=newest|popular|title, page, limit (≤100).
Accounts
# 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.
POST /accounts{username, email}creates the account and emails a code.POST /accounts/verify{email, code}returns the API key, shown once, and signs the browser in.- To sign in later:
POST /accounts/login-code{email}emails a fresh code, thenPOST /accounts/session{email, code}sets the session cookie. Codes last 10 minutes and burn after five wrong guesses. PATCH /me{name}sets the display name shown on your assets; it starts as your username.
/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)
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
# 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.
- 200: the asset was not published, so the change is already applied.
- 202: the asset is live, so the change is queued for review and the response carries
pendingEditwith the fields that differ. The published version keeps serving, unchanged, until a human approves it. Send anotherPATCHto revise the queued version, orDELETE /assets/{slug}/editto withdraw it and keep what is live.
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.
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
- Binary glTF 2.0 (.glb), ≤ 5 MB, self-contained, with no external URIs for buffers or images.
- ≤ 1,000,000 triangles, ≤ 32 textures at ≤ 4096², ≤ 64 materials.
- Draco / meshopt inputs are accepted and decoded; KTX2/Basis textures and specular-glossiness materials are not (yet).
- Passes the Khronos glTF validator with zero errors.
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.
| What | Anonymous (per IP) | With an API key |
|---|---|---|
| Search & read GET /api/v1/*, /assets/{slug}.md, MCP read tools | 60/min · 1,000/hour | 120/min · 3,000/hour |
| Downloads /download/{slug} | 30/min · 300/hour · 1,000/day | 60/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/day | 20/min · 150/hour · 400/day |
| Accounts POST /api/v1/accounts/verify, /accounts/session, key rotation | 10/hour · 30/day | 10/hour · 30/day |
| Account emails POST /api/v1/accounts, /accounts/login-code, /accounts/resend | 6/hour · 20/day | 6/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:
{ "error": { "code": "RATE_LIMITED", "message": "Rate limit exceeded for downloads (…). Retry in 42s.",
"details": { "bucket": "download", "retryAfterSeconds": 42, "limit": 30, "window": "minute" } } }- Wait for
Retry-Afterbefore retrying. Do not retry in a tight loop, and do not spread one job over several keys or IPs. - Fetching a whole pack is fine: a full pack manifest and its files fit inside the download window with room to spare.
- Use
cdnUrlin code anddownloadUrlfor human download buttons. CDN URLs are immutable and cacheable: fetch a model once and keep it, rather than re-fetching it per build. - Publishing runs a human review, so submission limits are deliberately low. They are not a queue to work around.
- Anything that sends email (sign-up,
/accounts/login-code,/accounts/resend) is counted separately and far more tightly than checking a code, and there are further caps per recipient address and across the site as a whole. Ask for one code and wait for it; retrying in a loop only burns the allowance of the person you are helping. - The CDN sits behind Cloudflare and applies its own abuse protection independently of these numbers.
- Need more for a legitimate project (a mirror, a research dataset, a tool)? Ask on the contact page. That is cheaper for us than serving a scraper.
Other
GET /me/assets: your submissions with reasons for failures/rejections.POST /packs/submissionswithtitle,summaryand theassetsslugs of your own uploads: propose a pack. Reviewed as a whole; approval publishes any members still pending and creates the pack page.GET /me/packstracks proposals, andPATCH/DELETE /packs/submissions/{id}edit or withdraw one while it is open.POST /reports: flag an asset.GET /download/{slug}(not under /api): a counted 302 to the CDN; use it for human download buttons, usecdnUrlin code.
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.