# Upload API guide for agents

Read [the main skill](https://3dassets.dev/skill.md) for account setup and credential handling. This reference describes the existing upload API. Handle storage references and request fields yourself; ask the user only for files, CC0 acceptance and publishing rights, never internal upload keys.

Use the configured THREEJSASSETS_API_KEY as Bearer auth for https://3dassets.dev/api/v1 requests. Keep it out of chat, logs and source files. Never attach it to storage uploads or external downloads, or forward it across redirects.

## Submit a local GLB

Use only files the user has asked to share, their acceptance of CC0 1.0 Universal and their confirmed publishing rights. If that information is already supplied, proceed; ask only for missing decisions. The rights statement is:

> I confirm that I created this asset or hold the rights to publish it under CC0 1.0 Universal, and agree to dedicate it to the public domain under CC0 so anyone can use, modify and redistribute it for personal or commercial purposes without attribution. I understand this dedication is irrevocable and the asset will be reviewed before publishing.

Only set attestation: true when the user has accepted this statement. Submission goes through processing and human review; acceptance is not publication.

1. Ensure the model is a self-contained .glb with embedded textures, no external URIs, at most 26214400 bytes and 1000000 triangles. Do not silently alter the user's source model.
2. Fetch /categories and use a returned category slug. CC0 1.0 Universal is applied automatically; no licence or tags need to be supplied.
3. POST https://3dassets.dev/api/v1/uploads/presign with Bearer auth and JSON {"filename":"model.glb","contentLength":ACTUAL_BYTE_COUNT,"withPreview":false}.
4. The response has upload.url, upload.key, upload.method and upload.headers (no data wrapper). PUT the raw GLB bytes to upload.url using precisely upload.headers. Do not send the site API key. Do not use multipart form data. Check the PUT succeeded before continuing.
5. POST https://3dassets.dev/api/v1/assets with Bearer auth and JSON:

```json
{
  "title": "Wooden park bench",
  "summary": "A low-poly wooden park bench with embedded textures.",
  "category": "CATEGORY_SLUG_FROM_API",
  "aiGenerated": false,
  "quarantineKey": "UPLOAD_KEY_FROM_STEP_3",
  "attestation": true
}
```

Optionally include aiModel (up to 160 characters) with an OpenRouter model ID from GET /api/v1/ai-models?q=... or a custom model/tool name. Set aiGenerated: true when AI helped produce the geometry, its generating code, or textures. Model attribution is contributor-reported, not independently verified. Omit aiModel if unknown; do not guess. Turning AI use off clears model attribution. Suggestions are refreshed hourly and are optional even when the catalogue is unavailable.

Set aiGenerated according to how the model was made; an agent uploading a human-made model does not make it AI-generated. Optional description is Markdown (scale, units, pivot and intended use). For a public HTTPS GLB, use POST /assets/from-url with the same metadata plus url, omitting quarantineKey; the service fetches and validates it.

## Batches and packs

Several files are several submissions: repeat the presign, PUT and create steps per file (two counted writes each) and pace yourself against the limits below. When the files belong together as a kit, propose a pack once every create has returned a slug: POST https://3dassets.dev/api/v1/packs/submissions with Bearer auth and JSON {"title":"...","summary":"...","assets":["slug-1","slug-2"]} (2 to 50 of the user's own slugs, in display order; optional description in Markdown). Members may still be processing or pending. A person reviews the pack as a whole: approval publishes any members still pending and creates the pack page; rejection leaves the models in the individual queue. At most 5 undecided proposals per account. GET https://3dassets.dev/api/v1/me/packs tracks them, and PATCH or DELETE https://3dassets.dev/api/v1/packs/submissions/{id} edits or withdraws one while it is open. Over MCP the same is submit_pack and my_packs.

## Results and retries

A 202 response is a submission receipt. Report its actual status and slug; never claim it is live until published. GET https://3dassets.dev/api/v1/me/assets returns the user's latest 100 assets with statuses and available failure/review details. GET /assets/{slug} with Bearer auth can retrieve an individual owned submission. Processing and review are asynchronous. Check status when needed for the requested task; do not install recurring jobs.

Errors are {"error":{"code":"...","message":"...","details":...}}. On 401, check the configured key; on 403, explain the verification or permission requirement; on 422, correct the reported fields. On 429 (code RATE_LIMITED) the submission limit is 60/min: wait the number of seconds in Retry-After, tell the user, and do not retry sooner, switch keys or split the batch across addresses. Submit files one at a time rather than in parallel. If a submission times out, check existing submissions before retrying: duplicate protection is not guaranteed. If success remains uncertain, report that rather than repeatedly submitting. Refresh an expired upload URL only if a new upload is needed.

