Every endpoint of the HTTP API with its parameters, an example request and response, and its errors. New to it? The quick start takes four requests. For AI agents, use the MCP server instead.
On this page
Basics
Overview
Base URL
https://3d.sleipner.ai
Versioning
One version, with no version in the path. New fields can appear in answers: ignore the ones you don’t know.
Requests
JSON, UTF-8, with Content-Type: application/json. Requests with images are multipart/form-data.
Responses
JSON, never cached (Cache-Control: no-store), except the download, which is a 303 redirect. Field names are as shown: snake_case in the answer to a make or change and in credits, camelCase on an object.
Ids
Objects and jobs have UUIDs. The ids in the examples here are made up.
Basics
Authentication
Send your API key as a bearer token on every request. Create keys in Settings under API keys (up to 10); a key starts with slp_ and is shown once, so keep it in an environment variable, never in code you share. A key acts as you: it spends your credits and sees your objects, but can’t buy credits, manage keys or change your account. Revoking a key works from the next request.
POST /api/objects takes a key only. The other endpoints also accept the signed-in browser session, which is what the site itself uses. A missing, wrong or revoked key is a 401:
Response
HTTP/1.1401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token"
{"error":"Invalid or revoked API key."}
Basics
Safe retries
Give every make or change a request_id (1–64 characters, your choice): an idempotency key, per API key. Sending the same id again returns the first object with 200 and "already_submitted": true, and charges nothing, so a timeout or a dropped connection can always be retried. Without one, every request makes a new object.
Basics
Rate limits
Per key: 60 requests at once, then 1 a second.
Making (POST /api/objects): 5 at once, then one every 30 seconds, per account. A request the capabilities or picture check holds up (a 422 scope_*, too_many_checks, images_unchecked) doesn't count; every other POST does, even a refused one.
Up to 2 objects queued or being made at once, per account.
Per IP address, for requests that carry a key: 120 at once, then 2 a second.
Over a rate limit the answer is 429 with Retry-After in seconds; there are no other rate-limit headers. Back off and try again; with a request_id a retried make is never charged twice. Need more? Tell us.
Response
HTTP/1.1429 Too Many Requests
Retry-After: 30
{"error":"Too many requests. Wait a minute and try again."}
Basics
Credits and charging
The API spends the same credits as the site, at the same prices. Credits are taken when a make or change is accepted and come back in full if the object fails or you cancel it. A job keeps the price it was submitted at. Everything that isn’t a make or change is free.
Call
Credits
Make, Quick
350200was 350, now 200
Make, Standard
1,000600was 1,000, now 600
Change, surface only
50 in the end (the tier’s price may be held at the start; the rest comes back)
An error is an HTTP status with a JSON body: error, a sentence you can show a person. A refusal to make or change something also has a code to branch on, and some add fields (a 402 adds balance and needed). Nothing is charged on an error.
Response
HTTP/1.1402 Payment Required
{"error":"This needs 600 credits and you have 500.","code":"insufficient_credits","balance":500,"needed":600}
Status
code
When
400
-
The body or a field is wrong ("Bad request: …"), or the request is a JSON product (send photos as multipart).
400
invalid
Making refused the input: a description outside 3-600 characters, a change outside 2-300, an object that isn't finished yet, an unknown tier.
401
-
No key, or a key that is wrong or revoked.
402
insufficient_credits
Not enough credits. The body has your balance and what is needed.
404
not_found
No such object or library model (on make or change); other routes answer 404 with no code.
422
scope_*
Held by the capabilities check (scope_scene, scope_structure, scope_vegetation, scope_animation, scope_effect, scope_text, scope_precision, scope_prohibited). The body's scope has status (uncertain, not_yet or refused), reason, suggestions, roadmap, notify_url and proceed_with: the flag that makes the same request go ahead (accept_uncertain or make_anyway; none when refused). Nothing is charged.
422
share_not_eligible
share: true on something that can't be shared: made from photos or reference images (or a change of one), or a request made with make_anyway or accept_uncertain.
403
share_paused
Sharing is paused on your account for a while, after several shares that weren't accepted. Make it without share.
409
share_unavailable
Community sharing isn't open. Make it without share.
400
images_unchecked
Reference images or a product's photos: the picture check couldn't run. Try again (or leave reference images out). Nothing is charged.
422
scope_prohibited
Reference images or a product's photos: a picture goes against the terms (someone else's character, brand or design; a real person; sexual, hateful or illegal content; a weapon to manufacture). A product's own brand and logo are fine. Can't be overridden; nothing is charged.
429
-
A rate limit. Retry-After says how many seconds to wait.
429
too_many_checks
Many requests held by the capabilities check in a short time. Wait a few seconds.
429
too_many_active
Already 2 objects in the works on your account. Wait for one to finish (no Retry-After).
503
daily_budget
Making is paused for today because of high demand. Nothing is charged.
503
disabled
Making is switched off for a while. Nothing is charged.
The capabilities check (422)
Every make and change is checked against what V1 makes before anything is charged. A held request is a 422 whose scope says why, what to make instead, and which flag makes the same request go ahead. The whole list is GET /api/capabilities.
Response
HTTP/1.1422 Unprocessable Entity
{"error":"We can't guarantee results for a castle yet: …","code":"scope_structure","scope":{"status":"uncertain",// supported | uncertain | not_yet | refused"code":"scope_structure","category":"structure","kind":"large_building","capability":"large_building","message":"We can't guarantee results for a castle yet: …","reason":"We can't guarantee results for a castle yet: …","pitfalls":"Towers, windows and small details melt together, …","suggestions":["a small wooden cabin","a stone well","a wooden front door"],"suggestion":"a small wooden cabin","roadmap":"Later: buildings and streets","interest":"large-buildings","notify_url":"https://3d.sleipner.ai/updates?interest=large-buildings","can_override":false,// uncertain: use proceed_with, not make_anyway"proceed_with":"accept_uncertain","credits":"…"},"hint":"Results for this are not guaranteed. If the user accepts that, send the same request with accept_uncertain: true."}
scope.status: supported (made; a note may come with it as scope_note), uncertain (results vary), not_yet (V1 can't do it yet) or refused (against the terms).
scope.proceed_with names the one flag that makes the same request go ahead: "accept_uncertain" for uncertain, "make_anyway" for not_yet, null for refused (nothing makes it go ahead).
scope.can_override is true only for not_yet (make_anyway). On uncertain it is false: that does not mean the request can't go ahead, it means use proceed_with (accept_uncertain: true), not make_anyway.
scope.reason (also message), pitfalls (uncertain: what tends to go wrong), suggestions and suggestion (single objects V1 makes instead), roadmap, interest and notify_url (a page the user opens to be told when it's ready; an agent can't opt the user in), credits (nothing was charged).
Endpoints
Objects
Make an object
POST/api/objectsAPI key
Describe one object; it is made with your credits. JSON in, a 201 with the new object's id out.
Parameters
kind"generate"bodyRequired
Selects the action: "generate".
promptstringbodyRequired
One object, 3 to 600 characters.
tier"quick" | "standard"body
Default quick.
request_idstring, 1–64body
Recommended. Your id for this request: the same id again returns the first object and charges nothing.
accept_uncertainbooleanbody
After a 422 whose scope.status is uncertain (proceed_with "accept_uncertain"), send the same request with true once the user accepts that results vary.
make_anywaybooleanbody
After a 422 whose scope.status is not_yet (proceed_with "make_anyway"), send the same request with true to try it regardless.
sharebooleanbody
Share it with the community at the shared price. We review it first; in the beginning, review can take 1–5 days before a shared model is published. Not with reference images or photos, nor with make_anyway or accept_uncertain. Default false.
Prices: Quick 200 credits, Standard 600. They are taken when the request is accepted and come back in full if the object fails or you cancel it.
Every request first gets the capabilities check: something V1 can't make, or can't promise, is a 422 before anything is charged (see Errors). When it goes through with a caution, the answer adds scope_note and scope.
With share: true the price is the shared one and the answer adds shared: true and a share_note; follow the review in share on the object.
Example request
curl
curl-X POST https://3d.sleipner.ai/api/objects \
-H"Authorization: Bearer $SLEIPNER_API_KEY"\
-H"Content-Type: application/json"\
-d'{"kind":"generate","prompt":"A brass ship lantern with a glass chimney","tier":"quick","request_id":"lantern-1"}'
Example response
201 Created
HTTP/1.1201 Created
{"object_id":"6f1c2d3e-4a5b-4c6d-8e7f-0a1b2c3d4e5f","status":"queued","page_url":"https://3d.sleipner.ai/objects/6f1c2d3e-4a5b-4c6d-8e7f-0a1b2c3d4e5f","api_url":"https://3d.sleipner.ai/api/objects/6f1c2d3e-4a5b-4c6d-8e7f-0a1b2c3d4e5f","price":200,// credits charged"list_price":350,// the list price (above price during the launch)"balance":400,// your balance afterwards"already_submitted":false}
200 OK · same request_id again
HTTP/1.1200 OK
{"object_id":"6f1c2d3e-4a5b-4c6d-8e7f-0a1b2c3d4e5f","status":"queued","page_url":"https://3d.sleipner.ai/objects/6f1c2d3e-4a5b-4c6d-8e7f-0a1b2c3d4e5f","api_url":"https://3d.sleipner.ai/api/objects/6f1c2d3e-4a5b-4c6d-8e7f-0a1b2c3d4e5f","price":200,"list_price":350,"balance":400,"already_submitted":true// the first object; nothing charged}
Errors
Status
code
When
400
-
The body isn't valid: "Bad request: prompt …". The message names the field.
400
invalid
A description outside 3–600 characters, or an unknown tier.
402
insufficient_credits
Not enough credits; the body has balance and needed.
422
scope_*
Held by the capabilities check; nothing charged. Retry with the flag in scope.proceed_with.
429
too_many_active
2 objects already in the works.
Change an object
POST/api/objectsAPI key
One change to one of your ready objects or a library model. You get a new object; the source stays as it is.
Parameters
kind"edit"bodyRequired
Selects the action: "edit".
instructionstringbodyRequired
The change, 2 to 300 characters: "make it dark green".
object_idstringbody
One of your ready objects. Give this or library_slug.
library_slugstringbody
A library model, e.g. "farm-wagon" (from /library.json).
tier"quick" | "standard"body
Default quick. Sets the price if the shape has to change.
request_idstring, 1–64body
Recommended. Your id for this request: the same id again returns the first object and charges nothing.
accept_uncertainbooleanbody
After a 422 whose scope.status is uncertain (proceed_with "accept_uncertain"), send the same request with true once the user accepts that results vary.
make_anywaybooleanbody
After a 422 whose scope.status is not_yet (proceed_with "make_anyway"), send the same request with true to try it regardless.
sharebooleanbody
Share it with the community at the shared price. We review it first; in the beginning, review can take 1–5 days before a shared model is published. Not with reference images or photos, nor with make_anyway or accept_uncertain. Default false. Not for a change of an object made from photos or reference images.
A new colour, material or wear keeps the mesh and costs 50 credits. When that can't be told in advance, the tier's price is taken and the rest comes back when it finishes: the object then shows job.price (taken) and job.creditBack (returned).
Library slugs are in /library.json.
Example request
curl
curl-X POST https://3d.sleipner.ai/api/objects \
-H"Authorization: Bearer $SLEIPNER_API_KEY"\
-H"Content-Type: application/json"\
-d'{"kind":"edit","library_slug":"farm-wagon","instruction":"paint it dark green","tier":"quick","request_id":"wagon-green"}'
Example response
201 Created
HTTP/1.1201 Created
{"object_id":"7a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d","status":"queued","page_url":"https://3d.sleipner.ai/objects/7a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d","api_url":"https://3d.sleipner.ai/api/objects/7a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d","price":200,// the tier's price; the surface-only rest comes back"list_price":350,"balance":200,"already_submitted":false}
Errors
Status
code
When
400
-
Neither or both of object_id and library_slug: "Give either object_id or library_slug."
400
invalid
An instruction outside 2–300 characters, or a source that isn't ready yet.
404
not_found
No such object on your account, or no such library model.
402
insufficient_credits
Not enough credits.
422
scope_*
Held by the capabilities check; nothing charged.
Make or change with reference images
POST/api/objectsAPI key
The same request as multipart/form-data, with pictures that guide the style, materials and colours: 1–4 on a new object, 1 on a change.
Parameters
requestJSONmultipartRequired
The same JSON as Make an object or Change an object, as a part with type=application/json.
imagesfilemultipartRequired
JPEG, PNG or WebP; 256 to 8,192 px a side, at most 40 megapixels, up to 20 MB each and 40 MB together. Repeat the part for each picture.
The prompt decides what the object is; the pictures guide how it looks. Each picture is checked before anything is charged. share: true with images is refused (share_not_eligible).
Example request
curl
curl-X POST https://3d.sleipner.ai/api/objects \
-H"Authorization: Bearer $SLEIPNER_API_KEY"\
-F'request={"kind":"generate","prompt":"a mid-century armchair in this mustard fabric","tier":"quick","request_id":"chair-1"};type=application/json'\
-F[email protected]
Example response
201 Created
HTTP/1.1201 Created
{"object_id":"6f1c2d3e-4a5b-4c6d-8e7f-0a1b2c3d4e5f","status":"queued","page_url":"https://3d.sleipner.ai/objects/6f1c2d3e-4a5b-4c6d-8e7f-0a1b2c3d4e5f","api_url":"https://3d.sleipner.ai/api/objects/6f1c2d3e-4a5b-4c6d-8e7f-0a1b2c3d4e5f","price":200,"list_price":350,"balance":400,"already_submitted":false}
Errors
Status
code
When
400
-
No images part, or reference images are switched off.
400
images_unchecked
The picture check couldn't run. Try again, or leave the images out.
422
scope_prohibited
A picture goes against the terms. Can't be overridden.
422
share_not_eligible
share: true with images.
List your objects
GET/api/objectsAPI key or session
Your objects, newest first, each in the same shape as Get an object.
HTTP/1.1200 OK
{"objects":[{"id":"6f1c2d3e-4a5b-4c6d-8e7f-0a1b2c3d4e5f","title":"Brass ship lantern","status":"ready", … }]}
Errors
Status
code
When
401
-
No key or session.
Get an object
GET/api/objects/{id}API key or session
Status and progress while it is made; renders, size and files once it is ready. Poll every 10 to 15 seconds.
Parameters
idstringpathRequired
The object_id from the make or change.
status is queued, running, ready or failed. A canceled object is failed with job.status canceled. A Quick object usually takes under 10 minutes, Standard 10–20.
job.status is queued, running, succeeded, failed or canceled; job.fraction (0 to 1) and job.stepLabel say how far it is; job.error says why it failed.
When ready, files has the poster, a light web GLB, the concept and renders (links valid one hour), size in metres, and variants: one per format and detail level. A variant's download is the website's link, which needs a browser sign-in; from code, use Download a file.
A shared object has job.shared true and a share with its review status (pending, needs_human, published, rejected, hidden or withdrawn), a label and detail, the reason when it wasn't accepted, and once published its public_path.
HTTP/1.1200 OK
{"id":"6f1c2d3e-4a5b-4c6d-8e7f-0a1b2c3d4e5f","title":"Brass ship lantern","prompt":"A brass ship lantern with a glass chimney","kind":"generate","status":"running","url":"/objects/6f1c2d3e-4a5b-4c6d-8e7f-0a1b2c3d4e5f","createdAt":"2026-11-02T09:14:03.512Z","parentId":null,"librarySlug":null,"basedOn":null,"job":{"id":"c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f","status":"running","tier":"quick","price":200,"listPrice":350,"creditBack":0,"surfaceHold":false,"fraction":0.45,// 0..1"step":"shape","stepLabel":"Building the 3D shape","message":null,"error":null,"cancelRequested":false,"photos":null,"references":0,"hasUserImages":false,"shared":false,"steps":[{"name":"size","label":"Measuring","status":"succeeded"},{"name":"concept","label":"Drawing the concept","status":"succeeded"},{"name":"shape","label":"Building the 3D shape","status":"running"}]},"files":null,// null until ready"share":null}
200 OK · ready (files only)
HTTP/1.1200 OK
{
…
"status":"ready","files":{"poster":"<presigned URL, valid 1 hour>","web":"<presigned URL, valid 1 hour>","concept":"<presigned URL, valid 1 hour>","renders":[{"view":"front34","url":"<presigned URL, valid 1 hour>"},{"view":"back34","url":"<presigned URL, valid 1 hour>"}],"size":{"width":0.18,"height":0.42,"depth":0.18},// metres"model":"…",// what built the shape; "keep-mesh" after a surface-only change"variants":[{"format":"glb","detail":"standard","bytes":6402118,"triangles":48210,"label":"GLB","download":"/objects/6f1c2d3e-4a5b-4c6d-8e7f-0a1b2c3d4e5f/download?format=glb&detail=standard"},{"format":"glb","detail":"low","bytes":1288540,"triangles":9840,"label":"GLB","download":"/objects/6f1c2d3e-4a5b-4c6d-8e7f-0a1b2c3d4e5f/download?format=glb&detail=low"},{"format":"fbx","detail":"standard","bytes":7950332,"triangles":48210,"label":"FBX","download":"/objects/6f1c2d3e-4a5b-4c6d-8e7f-0a1b2c3d4e5f/download?format=fbx&detail=standard"}]}}
Errors
Status
code
When
404
-
No such object on your account: {"error": "Not found"}.
Download a file
GET/api/objects/{id}/downloadAPI key or session
A 303 redirect to the file, on a link that works for 5 minutes. Let your client follow it. Downloads are free.
HTTP/1.1303 See Other
Location: <presigned URL, valid 5 minutes>
Cache-Control: no-store
Errors
Status
code
When
404
-
The object isn't ready, isn't yours, or has no such file.
503
-
Downloads are unavailable for a moment.
Cancel an object
POST/api/objects/{id}/cancelAPI key or session
Stops a queued or running object. Its credits come back.
Parameters
idstringpathRequired
The object to stop.
A queued object is canceled and refunded at once: {"status": "canceled"}. A running one answers {"status": "running"}: it stops within a minute and is refunded then. One that already finished answers with its final job status.
Example request
curl
curl-X POST -H"Authorization: Bearer $SLEIPNER_API_KEY"\
https://3d.sleipner.ai/api/objects/6f1c2d3e-4a5b-4c6d-8e7f-0a1b2c3d4e5f/cancel
Example response
200 OK
HTTP/1.1200 OK
{"status":"canceled"}
Errors
Status
code
When
404
-
No such object on your account.
Endpoints
Credits
Balance and prices
GET/api/creditsAPI key or session
Your balance, the price of each tier now, and your latest 20 credit entries.
Parameters
None.
credits is what is charged now; list_price is the list price, higher than credits during the launch (until launch_price_until, 2026-12-31). A job keeps the price it was submitted at.
Entry kinds: grant (free credits), purchase, spend, topup, refund, adjust, reversal. A key can read the balance but never buy credits.
HTTP/1.1200 OK
{"balance":400,"prices":[{"tier":"quick","label":"Quick","credits":200,"list_price":350,"launch_price_until":"2026-12-31"},{"tier":"standard","label":"Standard","credits":600,"list_price":1000,"launch_price_until":"2026-12-31"},{"tier":"accurate","label":"Accurate","credits":5000,"list_price":5000}],"entries":[{"kind":"spend","amount":-200,"note":"New object (Quick)","object_job_id":"c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f","created_at":"2026-11-02T09:14:03.498Z"},{"kind":"grant","amount":600,"note":"Free credits","object_job_id":null,"created_at":"2026-11-01T17:02:41.120Z"}]}
Errors
Status
code
When
401
-
No key or session.
Redeem a credit code
POST/api/credits/redeemAPI key or session
Adds a code's free credits to your account. Each code works once per account.
Parameters
codestringbodyRequired
The code, for example from a game jam.
JSON only: Content-Type must be application/json.
Example request
curl
curl-X POST https://3d.sleipner.ai/api/credits/redeem \
-H"Authorization: Bearer $SLEIPNER_API_KEY"\
-H"Content-Type: application/json"\
-d'{"code":"JAMEXAMPLE26"}'
Example response
200 OK
HTTP/1.1200 OK
{"credits":600,"balance":1000,"message":"600 credits added. They never expire."}
Errors
Status
code
When
400
-
No code, or a code that isn't valid or has run out (one message for both).
409
-
You've already used this code.
415
-
The body isn't JSON.
429
-
Too many tries; Retry-After says how long to wait.
Endpoints
Public
Capabilities
GET/api/capabilitiesNo auth
What V1 makes, may get wrong (uncertain) and can't do yet (not_yet), with examples, pitfalls and roadmaps. The same list the capabilities check uses.
Parameters
None.
Public and cached for an hour. MCP clients get the same from the list_capabilities tool.
Example request
curl
curl https://3d.sleipner.ai/api/capabilities
Example response
200 OK (shortened)
HTTP/1.1200 OK
{"version":"…","statuses":{"supported":"We make it. Just create it.","uncertain":"…","not_yet":"…"},"credits":"…","categories":[{"id":"…","title":"…","capabilities":[{"id":"…","status":"supported","title":"…","explanation":"…","examples":["…"],"roadmap":null,"interest":null}]}],"interests":[{"id":"…","title":"…"}]}
Library catalog
GET/library.jsonNo auth
Every free library model with its size, formats, detail levels and renders, plus the packs. Slugs here work as library_slug.
Parameters
None.
Downloading a library model needs a browser sign-in on the website, or the MCP tool get_download_link.
The whole flow in three languages: make an object, poll it, save the GLB.
curl
exportSLEIPNER_API_KEY=slp_...# 1. Make an objectcurl-s-X POST https://3d.sleipner.ai/api/objects \
-H"Authorization: Bearer $SLEIPNER_API_KEY"\
-H"Content-Type: application/json"\
-d'{"kind":"generate","prompt":"A brass ship lantern with a glass chimney","tier":"quick","request_id":"lantern-1"}'# 2. Follow it (every 10-15 s) until "status" is "ready" or "failed"curl-s-H"Authorization: Bearer $SLEIPNER_API_KEY" https://3d.sleipner.ai/api/objects/<object_id>
# 3. Download (-L follows the 303 to the file)curl-L-o lantern.glb -H"Authorization: Bearer $SLEIPNER_API_KEY"\
"https://3d.sleipner.ai/api/objects/<object_id>/download?format=glb&detail=standard"
JavaScript
// Node 18+ (fetch is built in). Save as make.mjs, run: node make.mjsimport{ writeFile }from"node:fs/promises";const API ="https://3d.sleipner.ai";const auth ={Authorization:`Bearer ${process.env.SLEIPNER_API_KEY}`};const res =awaitfetch(`${API}/api/objects`,{method:"POST",headers:{...auth,"Content-Type":"application/json"},body: JSON.stringify({kind:"generate",prompt:"A brass ship lantern with a glass chimney",tier:"quick",request_id:"lantern-1",// a retry with the same id returns the same object, charged once}),});const made =await res.json();if(!res.ok)thrownewError(`${res.status}${made.code ??""}: ${made.error}`);let object;do{awaitnewPromise((resolve)=>setTimeout(resolve,15_000));
object =await(awaitfetch(made.api_url,{headers: auth })).json();
console.log(object.status, object.job?.stepLabel ??"", Math.round((object.job?.fraction ??0)*100)+"%");}while(object.status ==="queued"|| object.status ==="running");if(object.status !=="ready")thrownewError(object.job?.error ??"The object failed; its credits were refunded.");// fetch follows the 303 to the file and drops the key on the way (another host).const file =awaitfetch(`${API}/api/objects/${object.id}/download?format=glb&detail=standard`,{headers: auth });if(!file.ok)thrownewError(`download: ${file.status}`);awaitwriteFile("lantern.glb", Buffer.from(await file.arrayBuffer()));
Python
# Python 3 with requests (pip install requests)import os, time, requests
API ="https://3d.sleipner.ai"
s = requests.Session()
s.headers["Authorization"]=f"Bearer {os.environ['SLEIPNER_API_KEY']}"
r = s.post(f"{API}/api/objects", json={"kind":"generate","prompt":"A brass ship lantern with a glass chimney","tier":"quick","request_id":"lantern-1",# a retry with the same id returns the same object, charged once})
made = r.json()ifnot r.ok:raiseSystemExit(f"{r.status_code} {made.get('code', '')}: {made['error']}")whileTrue:
time.sleep(15)
obj = s.get(made["api_url"]).json()
job = obj["job"]or{}print(obj["status"], job.get("stepLabel"),f"{round(job.get('fraction', 0) * 100)}%")if obj["status"]notin("queued","running"):breakif obj["status"]!="ready":raiseSystemExit(job.get("error")or"The object failed; its credits were refunded.")# requests follows the 303 and drops the key when the host changes.
f = s.get(f"{API}/api/objects/{obj['id']}/download", params={"format":"glb","detail":"standard"})
f.raise_for_status()withopen("lantern.glb","wb")as out:
out.write(f.content)