Online 3D Libraries
Search free 3D models in Poly Haven, Sketchfab and Smithsonian Open Access, and import one into the workspace's library as a normal 3D asset. This is the API behind Assets → 3D Models → Browse Online.
Session authentication only
These endpoints accept Authorization: Bearer <access token> only — X-API-Key is not accepted. Imports count toward the plan's 3D model limit, and Sketchfab downloads use the signed-in person's own Sketchfab account.
Search
Every library is queried at the same time and the results are interleaved. A library that fails reports its own status — the others' results still come back. Results never contain a download URL.
const params = new URLSearchParams({
q: 'chair',
providers: 'polyhaven,sketchfab,smithsonian', // optional, default: all
category: 'furniture', // optional: furniture, architecture, vehicles, nature, animals,
// people, electronics, food, fashion, art, history, props
license: 'cc0', // optional: cc0, by, by-sa, by-nd, …
commercial: '1', // default 1 — hide models without a clear commercial right
format: 'glb', // optional: glb | gltf
maxPolygons: '50000', // optional
pageSize: '24' // per library, max 48
});
const res = await fetch(BASE_URL + '/external-assets/search?' + params, {
headers: { 'Authorization': 'Bearer ' + accessToken }
}).then(r => r.json());
// { success: true,
// items: [{
// provider: "polyhaven", providerAssetId: "modern_arm_chair_01",
// name: "Modern Arm Chair 01", author: "Vibrant Nordic",
// sourceUrl, thumbnailUrl, tags, categories,
// license: "CC0 1.0 (Public Domain Dedication)", licenseId: "cc0",
// licenseTier: "allowed" | "conditions" | "blocked",
// commercialUseAllowed: true, attributionRequired: false,
// availableFormats: ["glTF"], polygonCount: 9000, // only when the library reports it
// importable: true, notImportableReason?: "…" }],
// nextCursor: "…" | null, // pass back as ?cursor= with the SAME filters
// providers: [{ provider: "sketchfab", status: "ok" | "error" | "skipped" | "exhausted",
// count: 19, total?: 24, message?, notices? }] }
// 60 searches per minute per person (429 rate_limited).Model details
const res = await fetch(BASE_URL + '/external-assets/items/sketchfab/' + encodeURIComponent(id), {
headers: { 'Authorization': 'Bearer ' + accessToken }
}).then(r => r.json());
// { success: true, asset: { …same shape as a search item, more detail… },
// alreadyImported: { assetId } | null, // this workspace imported it before
// provider: { name, credit, homepage } }
// The libraries and their state for this person:
// GET /external-assets/providers
// → { providers: [{ id, name, homepage, credit, requiresAccountForImport,
// supports: { category, license, maxPolygons, format }, // filters applied by the library
// importAvailable,
// connection?: { connected, username, displayName, profileUrl, connectedAt }, // Sketchfab
// notice? }], categories: [...] }Import
Send the library and the model id — never a URL. The server reads the model from the library again, checks its license, downloads it with size and time limits, converts glTF to GLB, runs the same checks as an upload and saves a normal 3D asset. The import runs as a background job you poll.
const res = await fetch(BASE_URL + '/external-assets/imports', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + accessToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({
provider: 'polyhaven',
providerAssetId: 'modern_arm_chair_01',
acknowledgeLicense: false, // must be true for CC BY-SA / CC BY-ND models
category: 'furniture' // optional asset category id
})
});
const data = await res.json();
// 202 { success: true, job: { id, status: "queued", progress: 0, … } }
// 200 { success: true, reused: true, assetId, asset } // already imported
// Poll the job:
// GET /external-assets/imports/:jobId → { success: true, job }
// job.status: queued → downloading → processing → saving → completed | failed
// job.progress 0–100, job.stage (human text), job.bytesReceived / bytesTotal
// completed: job.assetId, job.readiness (Configurator Readiness report), job.notices
// failed: job.error { code, message } e.g. invalid_file, conversion_failed,
// too_large, unavailable, interrupted
// GET /external-assets/imports → the 20 most recent jobs
// Errors when starting:
// 400 invalid_request · 403 quota_exceeded (plan's 3D model limit)
// 403 license_blocked (NonCommercial, Editorial, store or unknown license)
// 409 license_ack_required · 409 auth_required (connect Sketchfab first)
// 404 not_found · 413 too_large · 422 not_importable
// 429 rate_limited (30 per 10 min) / too_many_imports (3 running at once)
// 503 provider_not_configured (Sketchfab imports not set up on the platform)
// These endpoints never answer 401 for a library's own auth failure — a
// Sketchfab token problem is 409 auth_required.Licenses
allowed— CC0, public domain, CC BY: import directly.conditions— CC BY-SA, CC BY-ND: sendacknowledgeLicense: trueafter showing the terms to the person.blocked— NonCommercial, Editorial, Sketchfab store licenses and unknown licenses: never importable.
The imported asset stores externalSource — library, model id, name, source URL, author, license, license URL, the rights flags, a ready-made attribution line and the library's required providerCredit — and the credit is written into the GLB's asset.copyright. Reproduce the attribution wherever you publish a CC BY or CC BY-SA model.
Sketchfab connection
Sketchfab downloads need the person's own Sketchfab account. The connection belongs to the person, not the workspace, and its tokens never leave the server.
// Current state (never returns tokens):
// GET /external-providers/sketchfab/connection
// → { success: true, available: true, connected: false }
// Start: returns Sketchfab's authorize URL (single-use, 10 minutes).
const { url } = await fetch(BASE_URL + '/external-providers/sketchfab/connect', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + accessToken }
}).then(r => r.json());
window.open(url, 'sketchfab', 'width=560,height=760');
// Sketchfab redirects to the platform's callback, which sends the browser to
// /dashboard/assets?externalProvider=sketchfab&status=connected|error
// Disconnect:
// DELETE /external-providers/sketchfab/connection