Control every API step
Create models, confirm uploads, build projects, start renders, inspect status, retry failures, and retrieve outputs through individual endpoints.
This guide walks through each API endpoint individually, giving you full control over the model upload, project creation, and rendering process.
Looking for a simpler approach? The Jobs endpoint combines all these steps into a single workflow.
Step 1: Upload a 3D Model
Uploading a model is a two-step process: first you create the model record and get a signed upload URL, then you upload the file directly to our storage.
1.1 Create the Model Record
Request:
POST https://api.glossi.io/api/v1/modelsHeaders:
| Header | Value |
|---|---|
X-API-Key | Your API key |
Content-Type | application/json |
Body:
{
"models": [
{
"name": "My Product",
"fileName": "product.glb",
"fileType": "glb"
}
]
}Example with cURL:
curl -X POST https://api.glossi.io/api/v1/models \
-H "X-API-Key: glsi_xxxxxxxxxx_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"models": [
{
"name": "My Product",
"fileName": "product.glb",
"fileType": "glb"
}
]
}'Response:
{
"models": [
{
"modelId": "abc123-uuid",
"name": "My Product",
"uploadUrl": "https://s3.amazonaws.com/...",
"uploadKey": "workspaces/.../model.glb"
}
]
}Save these values from the response:
modelId→ You'll use this in Steps 1.3 and 4uploadUrl→ You'll upload your file to this URL in Step 1.2
You can create up to 50 models per request. Source model files are limited to 1 GB each — larger uploads are rejected at confirmation with a 413 MODEL_FILE_TOO_LARGE error.
Note The
uploadUrlexpires after 1 hour. If you're uploading many files, make sure to complete the upload within this time window.
1.2 Upload the File to S3
Now upload your 3D file directly to the signed URL. This request goes directly to S3, not to Glossi's API.
Request:
PUT <uploadUrl from previous response>Headers:
| Header | Value |
|---|---|
Content-Type | model/gltf-binary (for GLB files) |
Body: Your binary file data
Example with cURL:
curl -X PUT "<uploadUrl>" \
-H "Content-Type: model/gltf-binary" \
--data-binary @/path/to/your/model.glbIn Postman:
- Set method to PUT
- Paste the
uploadUrlas the URL - Go to Headers and add
Content-Type: model/gltf-binary - Go to Body, select binary, and choose your file
- Click Send
Note Don't include your API key for this request - the signed URL already contains authentication.
A successful upload returns an empty response with status 200 OK.
1.3 Confirm the Upload
After uploading to S3, call this endpoint to finalize the model. This updates the file path in our database and marks the model as ready for use in projects.
Request:
POST https://api.glossi.io/api/v1/models/{modelId}/confirmHeaders:
| Header | Value |
|---|---|
X-API-Key | Your API key |
Content-Type | application/json |
Body:
{
"fileType": "glb"
}Example with cURL:
curl -X POST https://api.glossi.io/api/v1/models/abc123-uuid/confirm \
-H "X-API-Key: glsi_xxxxxxxxxx_xxxxx" \
-H "Content-Type: application/json" \
-d '{"fileType": "glb"}'Response:
{
"modelId": "abc123-uuid",
"name": "My Product",
"status": "PENDING",
"filePath": "workspaces/.../model.glb"
}Your model upload is confirmed, but it still needs to be processed before you can use it in a project.
Note Processing starts as soon as the file lands in storage, so the
statusin this response may already beANALYZINGor evenREADYfor small models — not justPENDING.
Step 2: Wait for Model Processing (Important!)
After confirming the upload, the model goes through processing. You must wait until it's ready before creating a project.
Request:
POST https://api.glossi.io/api/v1/models/statusHeaders:
| Header | Value |
|---|---|
X-API-Key | Your API key |
Content-Type | application/json |
Body:
{
"modelIds": ["abc123-uuid"]
}Example with cURL:
curl -X POST https://api.glossi.io/api/v1/models/status \
-H "X-API-Key: glsi_xxxxxxxxxx_xxxxx" \
-H "Content-Type: application/json" \
-d '{"modelIds": ["abc123-uuid"]}'Response:
{
"models": [
{
"modelId": "abc123-uuid",
"name": "My Product",
"status": "READY",
"ready": true
}
],
"allReady": true,
"notFound": []
}Key fields:
ready:truewhen the model can be used in a projectallReady:truewhen ALL models in the request are readystatus: Current processing status (PENDING,ANALYZING,READY, etc.)
Polling Example
In automation tools like n8n, poll every 5-10 seconds until allReady is true:
# Simple polling loop
while true; do
RESPONSE=$(curl -s -X POST https://api.glossi.io/api/v1/models/status \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"modelIds": ["abc123-uuid"]}')
ALL_READY=$(echo $RESPONSE | jq -r '.allReady')
if [ "$ALL_READY" = "true" ]; then
echo "Model ready! Proceeding to create project..."
break
fi
echo "Model still processing..."
sleep 5
doneNote Processing time depends on source complexity, texture payload, conversion capacity, and format. Poll the model state instead of assuming a fixed duration.
Step 3: Find a Template
Before creating a project, you'll need a template ID. Templates define the lighting, camera angles, and background for your renders. You can list available templates via the API.
Request:
GET https://api.glossi.io/api/v1/templatesHeaders:
| Header | Value |
|---|---|
X-API-Key | Your API key |
Example with cURL:
curl https://api.glossi.io/api/v1/templates \
-H "X-API-Key: glsi_xxxxxxxxxx_xxxxx"Response:
{
"templates": [
{
"templateId": "abc123-template",
"name": "Studio Light",
"description": "Clean studio lighting setup"
},
{
"templateId": "def456-template",
"name": "Outdoor Scene",
"description": "Natural outdoor lighting"
}
]
}Save the templateId for the template you want to use in Step 4.
Filtering Templates
By default, you'll see both public templates (available to everyone) and any custom templates in your workspace. You can filter these:
# Only public templates
curl "https://api.glossi.io/api/v1/templates?internal=false" \
-H "X-API-Key: glsi_xxxxxxxxxx_xxxxx"
# Only your workspace templates
curl "https://api.glossi.io/api/v1/templates?public=false" \
-H "X-API-Key: glsi_xxxxxxxxxx_xxxxx"Step 4: Create a Project
A project combines your 3D model with a template (lighting, camera angles, background). You'll need the modelId you saved from Step 1.1.
Important Your model must be in
READYstatus before you can create a project with it. See Step 2 for how to check model status.
Request:
POST https://api.glossi.io/api/v1/projectsHeaders:
| Header | Value |
|---|---|
X-API-Key | Your API key |
Content-Type | application/json |
Body:
{
"name": "Product Photoshoot",
"modelIds": ["abc123-uuid"],
"templateId": "your-template-uuid"
}Example with cURL:
curl -X POST https://api.glossi.io/api/v1/projects \
-H "X-API-Key: glsi_xxxxxxxxxx_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Product Photoshoot",
"modelIds": ["abc123-uuid"],
"templateId": "template-uuid"
}'Response:
{
"projects": [
{
"projectId": "project-uuid",
"name": "Product Photoshoot",
"modelIds": ["abc123-uuid"],
"templateId": "template-uuid"
}
]
}Save the projectId - you'll use it in Step 5 to start a render.
Creating Multiple Projects at Once
If you have multiple models and want a separate project for each (up to 50 models per request):
{
"modelIds": ["model-1-uuid", "model-2-uuid", "model-3-uuid"],
"templateId": "template-uuid",
"createPerModel": true
}This creates 3 projects, one for each model.
Step 5: Start a Render
Now you can render your project to generate images and videos. Use the projectId you saved from Step 4.
Request:
POST https://api.glossi.io/api/v1/rendersHeaders:
| Header | Value |
|---|---|
X-API-Key | Your API key |
Content-Type | application/json |
Body:
{
"projectIds": ["project-uuid"],
"settings": {
"renderBookmarks": true,
"renderShots": true,
"renderVariants": true,
"imageQuality": 1,
"videoQuality": 1
}
}Render Settings Explained
| Setting | Default | Options |
|---|---|---|
renderBookmarks | true | Render all saved camera angles (images) |
renderShots | true | Render all video shots |
renderVariants | true | Render all material/color variants |
imageQuality | 1 | 0=720p, 1=1080p, 2=4K |
imageFormat | "PNG" | "PNG" (8-bit) or "TIFF" (16-bit) |
videoQuality | 1 | 0=Fast (preview), 1=Best (production) |
videoResolution | 1 | 0=720p, 1=1080p, 2=4K |
Example - Render images only at 4K:
curl -X POST https://api.glossi.io/api/v1/renders \
-H "X-API-Key: glsi_xxxxxxxxxx_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"projectIds": ["project-uuid"],
"settings": {
"renderBookmarks": true,
"renderShots": false,
"imageQuality": 2
}
}'Response:
{
"batchId": "batch-uuid",
"jobs": [
{
"jobId": "job-uuid",
"status": "QUEUED",
"projectId": "project-uuid"
}
],
"totalProjects": 1
}Save the jobId - you'll use it in Step 6 to check the render status.
Rendering Multiple Projects
You can render up to 50 projects in a single request (duplicate IDs are de-duplicated):
{
"projectIds": ["project-1", "project-2", "project-3"],
"settings": {
"renderBookmarks": true,
"imageQuality": 1
}
}Each project gets its own render job, all sharing the batchId from the response. To poll them together, use the batch endpoint instead of polling each job:
GET https://api.glossi.io/api/v1/renders/batch/{batchId}{
"batchId": "batch-uuid",
"total": 3,
"counts": { "QUEUED": 1, "RENDERING": 1, "COMPLETED": 1 },
"jobs": [
{ "jobId": "job-1", "status": "COMPLETED", "projectId": "project-1" },
{ "jobId": "job-2", "status": "RENDERING", "projectId": "project-2" },
{ "jobId": "job-3", "status": "QUEUED", "projectId": "project-3" }
]
}Step 6: Check Render Status
Renders take time to complete. Use the jobId from Step 5 to poll for progress.
Request:
GET https://api.glossi.io/api/v1/renders/{jobId}Headers:
| Header | Value |
|---|---|
X-API-Key | Your API key |
Example with cURL:
curl https://api.glossi.io/api/v1/renders/job-uuid \
-H "X-API-Key: glsi_xxxxxxxxxx_xxxxx"Response (in progress):
{
"jobId": "job-uuid",
"status": "RENDERING",
"projectId": "project-uuid"
}Response (complete):
{
"jobId": "job-uuid",
"status": "COMPLETED",
"projectId": "project-uuid",
"renders": [
{
"id": "render-uuid-1",
"name": "Front View - Default",
"filePath": "workspaces/.../render.png",
"fileType": "PNG",
"isVideo": false
},
{
"id": "render-uuid-2",
"name": "360 Spin - Default",
"filePath": "workspaces/.../video.mp4",
"fileType": "MP4",
"isVideo": true
}
]
}Key fields in renders:
name: Display name of the render (e.g., "Front View - Oak")filePath: Direct (unsigned) URL to the file in storagefileType: Format of the file (PNG,MP4,PRORES, etc.)isVideo:truefor videos,falsefor images
Note The
rendersarray contains all images and videos generated for this job (bookmarks, shots, and variants).
Downloading a Render
For a guaranteed-working download link, use the download endpoint with a render's id. It responds with a 302 redirect to a signed URL that's valid for 1 hour:
curl -L -o render.png "https://api.glossi.io/api/v1/renders/render-uuid-1/download" \
-H "X-API-Key: glsi_xxxxxxxxxx_xxxxx"(The -L flag tells cURL to follow the redirect.)
Job Statuses
| Status | Meaning |
|---|---|
QUEUED | Waiting to start |
RENDERING | Currently processing |
COMPLETED | Finished successfully |
FAILED | Something went wrong (check errorMessage) |
CANCELLED | Cancelled before rendering started |
Cancelling a Render
A render job can be cancelled while it's still QUEUED:
POST https://api.glossi.io/api/v1/renders/{jobId}/cancelReturns { "jobId": "...", "status": "CANCELLED" } on success, or 400 INVALID_STATUS if the job has already started. In practice the window is short - jobs usually move to RENDERING within seconds of being created - so treat cancellation as best-effort.
Retrying Failed Renders
If a render fails, you can retry it:
Request:
POST https://api.glossi.io/api/v1/renders/{jobId}/retryHeaders:
| Header | Value |
|---|---|
X-API-Key | Your API key |
Example with cURL:
curl -X POST https://api.glossi.io/api/v1/renders/job-uuid/retry \
-H "X-API-Key: glsi_xxxxxxxxxx_xxxxx"Response:
{
"jobId": "job-uuid",
"status": "QUEUED",
"projectId": "project-uuid",
"batchId": "new-batch-uuid"
}The job will be re-queued and you can poll for status as before. Note that only jobs with status FAILED can be retried.
Step 7: Set Up Webhooks (Optional)
Instead of polling, you can receive notifications when important events occur.
Full documentation See the Webhooks Guide for complete details on all events, payload formats, and signature verification.
Available Events
| Event | Description |
|---|---|
model.processed | Model upload processing completed successfully |
model.failed | Model processing failed |
project.created | A project was created |
job.complete | A job workflow completed successfully |
job.failed | A job workflow failed |
render.complete | A render completed successfully |
render.failed | A render failed |
Configure Your Webhook
Request:
PUT https://api.glossi.io/api/v1/webhooksBody:
{
"url": "https://your-app.com/webhooks/glossi",
"events": [
"model.processed",
"model.failed",
"project.created",
"job.complete",
"job.failed",
"render.complete",
"render.failed"
],
"enabled": true
}Response:
{
"id": "webhook-uuid",
"url": "https://your-app.com/webhooks/glossi",
"secret": "your-webhook-secret",
"events": ["model.processed", "job.complete", "render.complete", ...],
"enabled": true
}Save the secret - you'll use it to verify webhook signatures.
Webhook Payloads
When a model finishes processing:
{
"event": "model.processed",
"timestamp": "2026-07-28T10:30:00.000Z",
"data": {
"modelId": "model-uuid",
"name": "Chair Model",
"status": "READY",
"glbFilePath": "https://s3.../file.glb",
"thumbnailUrl": "https://s3.../file.jpg"
}
}When a job completes:
{
"event": "job.complete",
"timestamp": "2026-07-28T10:30:00.000Z",
"data": {
"jobId": "job-uuid",
"status": "COMPLETE",
"results": [
{
"model": { "id": "model-uuid", "name": "Chair" },
"project": { "id": "project-uuid", "name": "Chair Project" }
}
]
}
}When a render completes:
{
"event": "render.complete",
"timestamp": "2026-07-28T10:30:00.000Z",
"data": {
"jobId": "render-job-uuid",
"status": "COMPLETED",
"projectId": "project-uuid",
"renderIds": ["render-uuid-1", "render-uuid-2"]
}
}Verify Webhook Signatures
Always verify the X-Glossi-Signature header to ensure the request came from Glossi. Compute the HMAC over the raw request body bytes - not a re-serialized copy of the parsed JSON:
const crypto = require("crypto")
// rawBody is a Buffer or string of the exact bytes received
function verifyWebhook(rawBody, signature, secret) {
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex")
const a = Buffer.from(signature || "")
const b = Buffer.from(expected)
return a.length === b.length && crypto.timingSafeEqual(a, b)
}See the Webhooks Guide for complete examples in Node.js, Python, and PHP, including how to access the raw body in each framework.
Next Steps
- Webhooks Guide - Full webhook documentation
- Simple Workflow - Streamlined Jobs endpoint
- Getting Started - API key setup and overview