Generation status
Start a generation with /api/generate/start, poll GET /api/generate/status, and read the finished model URL back — with the real auth model and expiry rules.
A real generation takes longer than a proxy will hold a request open, so the working pattern is two calls: POST /api/generate/start hands you a jobId, and GET /api/generate/status?jobId= tells you where it got to. When the job finishes, its result field is the complete /api/generate response body, model URL included.
Start a job#
/api/generate/startSigned inQueue a generation and return immediately. Takes the same body as /api/generate.
The raw body and content type are captured and replayed against the generation pipeline in the background, so both the JSON text form and the multipart image form work unchanged. See /docs/api/generate for the fields. An empty body is 400 invalid body.
{ "jobId": "job_m2k1r8q_4f7a9c2e", "status": "queued" }Poll it#
/api/generate/status?jobId=Signed inReturn the job record, including the full generation result once it is done.
jobIdstringrequired- The id returned by /api/generate/start. Jobs are owned by the account that created them; asking for someone else's is 403.
{
"id": "job_m2k1r8q_4f7a9c2e",
"userEmail": "[email protected]",
"status": "done",
"createdAt": 1757404800000,
"startedAt": 1757404800412,
"completedAt": 1757404838907,
"result": { "modelUrl": "gs://…", "printStatus": "ready", "creditsCharged": 1, "…": "…" }
}The three timestamps are epoch milliseconds. startedAt, completedAt, result and error are only present once they apply.
| status | Meaning | What is set |
|---|---|---|
| queued | Accepted, not picked up yet. | createdAt only. |
| running | The pipeline is working — enhancing, generating, downloading, repairing, measuring. | startedAt. |
| done | Finished. Read result. | completedAt, result. |
| error | The generation failed, or the instance running it went away. Credits charged by a failed generation are refunded. | completedAt, error. |
error carries the message the generation route returned — an engine failure, a moderation refusal, a no_credits rejection — or "Generation was interrupted and did not finish." for a job swept after 30 minutes.
A polling loop#
JOB=$(curl -s https://x3dstudios.com/api/generate/start \
-H "Authorization: Bearer $X3D_EXT_TOKEN" \
-H "content-type: application/json" \
-d '{"prompt":"a low-poly owl figurine","mode":"fast_draft"}' | jq -r .jobId)
while :; do
BODY=$(curl -s "https://x3dstudios.com/api/generate/status?jobId=$JOB" \
-H "Authorization: Bearer $X3D_EXT_TOKEN")
case "$(echo "$BODY" | jq -r .status)" in
done) echo "$BODY" | jq -r '.result.modelUrl, .result.printStatus'; break ;;
error) echo "$BODY" | jq -r .error; exit 1 ;;
esac
sleep 5
doneFive seconds is a sensible interval. Status reads are not rate-limited, but nothing changes faster than the pipeline's own two-second engine poll, and a text draft rarely finishes in under twenty seconds.
Getting the model#
result.modelUrl is a gs:// object URI in our storage, not a public link. Fetch it through the proxy, which checks that the signed-in account owns that generation.
curl -G "https://x3dstudios.com/api/model-proxy" \
--data-urlencode "url=$MODEL_URL" \
--data-urlencode "download=true" \
-b "$COOKIE_JAR" -o model.glb| Field in result | Lifetime |
|---|---|
| modelUrl with persisted: true | A gs:// object we wrote. No expiry. This is the repaired, welded, oriented mesh — the one to print. |
| modelUrl with persisted: false | Storage failed, so this is the engine's own pre-signed URL. It expires within the hour and the model is then unrecoverable. Download it now. |
| previewUrl | The engine's original textured mesh, stored alongside. Repair drops UVs, so this is what a viewer should render. Equal to modelUrl when nothing was repaired. |
Is anything running?#
/api/generate/activeSigned inThe account's most recent queued or running job, if there is one.
Session cookie only — this one has no extension-token support. It never returns 401: signed out gives 200 {"active":false}. Signed in with a live job gives {"active":true,"jobId","status","createdAt"}. Jobs older than 30 minutes are ignored here, so a job orphaned by a lost instance cannot block the account forever.
Errors#
| Status | error | Endpoint |
|---|---|---|
| 401 | Authentication required | start and status. No session and no valid x3d_ext_ token. |
| 400 | invalid body | start, when the request body is empty. |
| 400 | jobId required | status, when the query parameter is missing. |
| 404 | not found | status, for an unknown job id. |
| 403 | forbidden | status, when the job belongs to another account. |
| 429 | Generation limit reached. Please try again later. | start. 20 an hour per account, with a Retry-After header. |