Dashboard →

API reference

Everything the dashboard does is available over HTTPS. The machine-readable document is at https://glboost.com/api/v1/openapi.json.

Authentication

Create a key under Settings → API keys. Keys carry read and/or write scopes and may be pinned to one project.

curl https://glboost.com/api/v1/assets \
  -H "Authorization: Bearer glb_live_xxxxxxxx_yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
  • Wrong or revoked key → 401. Missing scope → 403.
  • Rate limit: 120 requests per minute per key (token bucket, refills at 2/s). Exceeding it returns 429 with Retry-After: 1.
  • Errors are { "error": "...", "issues"?: [...] }; issues lists zod validation problems for 400s.

Upload in three calls

# 1. start
curl -X POST https://glboost.com/api/v1/uploads -H "Authorization: Bearer $KEY" -H "content-type: application/json" \
  -d '{"projectId":"<projectId>","filename":"helmet.glb","contentType":"model/gltf-binary","bytes":3773916}'
# → { assetId, versionId, uploadId, partSize, uploadUrl, token }

# 2. PUT each part (parts of partSize bytes; the last may be smaller), collect the etags
curl -X PUT "$UPLOAD_URL/part?n=1&token=$TOKEN" --data-binary @helmet.glb   # → { partNumber, etag }
curl -X POST "$UPLOAD_URL/complete?token=$TOKEN" -H "content-type: application/json" \
  -d '{"parts":[{"partNumber":1,"etag":"..."}]}'

# 3. finish: records the object and queues the project's default presets
curl -X POST https://glboost.com/api/v1/uploads/$VERSION_ID/complete -H "Authorization: Bearer $KEY"

Webhooks

Subscribe under Settings → Webhooks. Events: asset.ready, variant.ready, variant.failed, share.viewed, share.downloaded, comment.created, version.approved, version.changes_requested. Each POST carries X-GLBoost-Event, X-GLBoost-Delivery, and X-GLBoost-Signature: t=<unix>,v1=<hex> where v1 is HMAC-SHA256 over `${t}.${body}` with the webhook secret. Non-2xx answers are retried after 1 min, 5 min, 30 min, 2 h, and 12 h. Reject timestamps older than five minutes.

import { createHmac, timingSafeEqual } from 'node:crypto';

export function verify(secret: string, header: string, body: string): boolean {
  const parts = Object.fromEntries(header.split(',').map((kv) => kv.split('=')));
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
  const expected = createHmac('sha256', secret).update(`${parts.t}.${body}`).digest('hex');
  return expected.length === parts.v1?.length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Meta

get/api/v1/openapi.jsonpublic

This document

Responses
  • 200OpenAPI 3.1 document

Projects

get/api/v1/projectskey: read

List projects

Every project of the organization, oldest first; a key pinned to a project sees only that project. Asset routes work in the active project: the dashboard's `glboost_project` cookie, a key's pinned project, or the `X-GLBoost-Project` header (404 when it names a project outside the organization), else the oldest project.

Responses
  • 200Projects
post/api/v1/projectsdashboard only

Create a project

Pro and up (`maxProjects` per plan). Owners and admins only. The project gets its own signing key and delivery settings.

Request body (application/json)
{
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 100
    }
  },
  "required": [
    "name"
  ]
}
Responses
  • 201Created
  • 400Invalid body
  • 402Plan does not include this feature
get/api/v1/projects/{projectId}key: read

Get a project

Responses
  • 200Project
  • 404Not found
patch/api/v1/projects/{projectId}key: write

Update delivery settings

`lodRatios` (Pro and up) lists up to three simplification ratios; every upload then gets one `gpu`-based variant per ratio for the viewer to switch between. An empty list turns the ladder off. `publishOnApproval` (Pro and up) holds replacement versions until a reviewer approves: `owner` needs an owner or admin, `any` accepts anyone the share link admits; the first version of an asset always publishes.

Request body (application/json)
{
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 100
    },
    "defaultPreset": {
      "type": "string",
      "enum": [
        "compat",
        "web",
        "gpu",
        "mobile",
        "embedded",
        "original"
      ]
    },
    "defaultVariantPresets": {
      "minItems": 1,
      "maxItems": 6,
      "type": "array",
      "items": {
        "type": "string",
        "enum": [
          "compat",
          "web",
          "gpu",
          "mobile",
          "embedded",
          "original"
        ]
      }
    },
    "signingRequired": {
      "type": "boolean"
    },
    "allowedDomains": {
      "maxItems": 50,
      "type": "array",
      "items": {
        "type": "string",
        "maxLength": 200,
        "pattern": "^(\\*\\.)?[a-z0-9.-]+\\.[a-z]{2,}$|^localhost(:\\d+)?$"
      }
    },
    "lodRatios": {
      "maxItems": 3,
      "type": "array",
      "items": {
        "type": "number",
        "minimum": 0.1,
        "maximum": 0.9,
        "multipleOf": 0.05
      }
    },
    "autoUsdz": {
      "type": "boolean"
    },
    "publishOnApproval": {
      "type": "string",
      "enum": [
        "off",
        "owner",
        "any"
      ]
    }
  }
}
Responses
  • 200Project
  • 400Invalid body
  • 402Plan does not include this feature
  • 404Not found
delete/api/v1/projects/{projectId}dashboard only

Delete an empty project

Owners and admins only. 409 with `reason: "not-empty"` while the project holds assets (the trash included), collections, or environments, and `reason: "last-project"` for the organization's only project.

Responses
  • 200Deleted
  • 404Not found
  • 409Not empty, or the last project

Organizations

patch/api/v1/orgs/{orgId}key: write

Update organization branding

Request body (application/json)
{
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 100
    },
    "brandColor": {
      "anyOf": [
        {
          "type": "string",
          "pattern": "^#[0-9a-fA-F]{6}$"
        },
        {
          "type": "null"
        }
      ]
    }
  }
}
Responses
  • 200Organization
  • 400Invalid body
  • 404Not found

Uploads

post/api/v1/uploadskey: write

Start an upload

Creates the asset (or a new version when `assetId` is set) and returns a multipart upload target on the CDN: PUT each part to `{uploadUrl}/part?n=<1-based>&token=<token>` (parts of `partSize` bytes, last part may be smaller), then POST `{uploadUrl}/complete?token=<token>` with `{ parts: [{ partNumber, etag }] }`, then call `POST /api/v1/uploads/{versionId}/complete`. Accepted files: `.glb`, self-contained `.gltf`, or a `.zip` holding one `.gltf` (or `.glb`) and the files it references; a pack job turns the archive into the original GLB while the variants are optimized from it directly.

Request body (application/json)
{
  "type": "object",
  "properties": {
    "projectId": {
      "type": "string",
      "minLength": 1
    },
    "filename": {
      "type": "string",
      "minLength": 1,
      "maxLength": 200,
      "description": "Ends in .glb, .gltf, or .zip (a glTF with the files it references)"
    },
    "contentType": {
      "type": "string",
      "minLength": 1,
      "maxLength": 100
    },
    "bytes": {
      "type": "integer",
      "exclusiveMinimum": 0,
      "maximum": 9007199254740991
    },
    "assetId": {
      "type": "string",
      "minLength": 1
    },
    "note": {
      "anyOf": [
        {
          "type": "string",
          "maxLength": 2000
        },
        {
          "type": "null"
        }
      ]
    }
  },
  "required": [
    "projectId",
    "filename",
    "contentType",
    "bytes"
  ]
}
Responses
  • 200Upload target
  • 400Invalid body
  • 413File too large
  • 415Unsupported format
post/api/v1/uploads/{versionId}/completekey: write

Finish an upload and queue optimization

Responses
  • 200Queued jobs
  • 404Not found
  • 409Object not yet on the CDN, or the upload was aborted
post/api/v1/uploads/{versionId}/abortkey: write

Give up on an upload

Marks a version that is still `uploading` as `failed` so it no longer waits for parts. Idempotent. Uploads that are never completed or aborted are failed automatically once their token expires.

Responses
  • 200Version marked failed
  • 404Not found
  • 409The upload already completed

Assets

get/api/v1/assetskey: read

List or search assets

Query: `projectId` (defaults to the active project), `q` (case-insensitive substring of the name or description, or a prefix of the id), `tag` (exact), `deleted=true` (the trash instead of live assets). Items carry `tags`, `reviewState` (`approved`, `changes_requested`, `in_review`, or null for the newest version), `latestVersion`, and `deletedAt`.

Responses
  • 200Assets
  • 400Invalid body
get/api/v1/assets/{assetId}key: read

Get an asset with versions, variants, and review state

Each version carries `note` and `review` (`approved` and `changesRequested` counts, each reviewer's current stance, and `awaitingApproval` when publish-on-approval is holding it).

Responses
  • 200Asset detail
  • 404Not found
patch/api/v1/assets/{assetId}key: write

Update name, visibility, viewer settings, hotspots, tags; move between projects

`tags` replaces the labels (lower-case `a-z0-9_-`, up to 20; a bad tag answers 400 naming it). `projectId` moves the asset to another project of the organization: embeds keep working, but signed URLs minted with the old project's key stop verifying.

Request body (application/json)
{
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 200
    },
    "description": {
      "anyOf": [
        {
          "type": "string",
          "maxLength": 2000
        },
        {
          "type": "null"
        }
      ]
    },
    "visibility": {
      "type": "string",
      "enum": [
        "public",
        "private"
      ]
    },
    "viewerSettings": {
      "anyOf": [
        {
          "type": "object",
          "properties": {
            "camera": {
              "type": "object",
              "properties": {
                "yaw": {
                  "type": "number",
                  "minimum": -3600,
                  "maximum": 3600
                },
                "pitch": {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 180
                },
                "distance": {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 1000000
                },
                "fov": {
                  "type": "number",
                  "minimum": 1,
                  "maximum": 179
                }
              },
              "required": [
                "yaw",
                "pitch",
                "distance"
              ]
            },
            "target": {
              "type": "array",
              "prefixItems": [
                {
                  "type": "number"
                },
                {
                  "type": "number"
                },
                {
                  "type": "number"
                }
              ],
              "items": false,
              "minItems": 3,
              "maxItems": 3
            },
            "exposure": {
              "type": "number",
              "minimum": 0,
              "maximum": 2
            },
            "shadowIntensity": {
              "type": "number",
              "minimum": 0,
              "maximum": 1
            },
            "environment": {
              "type": "string",
              "maxLength": 2000
            },
            "background": {
              "type": "string",
              "maxLength": 64,
              "pattern": "^(transparent|#[0-9a-fA-F]{3,8}|[a-zA-Z]+|rgba?\\([\\d\\s.,%]+\\))$"
            },
            "autoRotate": {
              "type": "boolean"
            },
            "animation": {
              "anyOf": [
                {
                  "type": "string",
                  "maxLength": 200
                },
                {
                  "type": "null"
                }
              ]
            },
            "variant": {
              "anyOf": [
                {
                  "type": "string",
                  "maxLength": 200
                },
                {
                  "type": "null"
                }
              ]
            },
            "ar": {
              "type": "boolean"
            },
            "variantPicker": {
              "type": "object",
              "properties": {
                "enabled": {
                  "type": "boolean"
                },
                "labels": {
                  "type": "object",
                  "propertyNames": {
                    "type": "string",
                    "maxLength": 64
                  },
                  "additionalProperties": {
                    "type": "string",
                    "maxLength": 64
                  }
                },
                "swatches": {
                  "type": "object",
                  "propertyNames": {
                    "type": "string",
                    "maxLength": 64
                  },
                  "additionalProperties": {
                    "type": "string",
                    "maxLength": 2000,
                    "pattern": "^(#[0-9a-fA-F]{3,8}|[a-zA-Z]+|rgba?\\([\\d\\s.,%]+\\)|https:\\/\\/.+)$"
                  }
                }
              },
              "required": [
                "enabled"
              ]
            }
          }
        },
        {
          "type": "null"
        }
      ]
    },
    "hotspots": {
      "maxItems": 50,
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "body": {
            "type": "string",
            "maxLength": 2000
          },
          "position": {
            "type": "array",
            "prefixItems": [
              {
                "type": "number"
              },
              {
                "type": "number"
              },
              {
                "type": "number"
              }
            ],
            "items": false,
            "minItems": 3,
            "maxItems": 3
          },
          "normal": {
            "type": "array",
            "prefixItems": [
              {
                "type": "number"
              },
              {
                "type": "number"
              },
              {
                "type": "number"
              }
            ],
            "items": false,
            "minItems": 3,
            "maxItems": 3
          },
          "camera": {
            "type": "object",
            "properties": {
              "yaw": {
                "type": "number",
                "minimum": -3600,
                "maximum": 3600
              },
              "pitch": {
                "type": "number",
                "minimum": 0,
                "maximum": 180
              },
              "distance": {
                "type": "number",
                "minimum": 0,
                "maximum": 1000000
              },
              "fov": {
                "type": "number",
                "minimum": 1,
                "maximum": 179
              }
            },
            "required": [
              "yaw",
              "pitch",
              "distance"
            ]
          },
          "variant": {
            "type": "string",
            "maxLength": 200
          }
        },
        "required": [
          "id",
          "title",
          "position",
          "normal"
        ]
      }
    },
    "tags": {
      "maxItems": 20,
      "type": "array",
      "items": {
        "type": "string",
        "maxLength": 64
      }
    },
    "projectId": {
      "type": "string",
      "minLength": 1
    }
  }
}
Responses
  • 200Asset
  • 400Invalid body
  • 404Not found
delete/api/v1/assets/{assetId}key: write

Move an asset to the trash, or purge it

Moves the asset to the trash, where it stays restorable for 30 days before the nightly purge. `?permanent=1` deletes a trashed asset and its files for good; a live asset answers 409, so deletion is always two steps.

Responses
  • 200Deleted
  • 404Not found
  • 409Live asset with `permanent=1`
post/api/v1/assets/{assetId}/restorekey: write

Restore an asset from the trash

Responses
  • 200Asset
  • 404Not found
post/api/v1/assets/{assetId}/thumbnail/renderkey: write

Render the poster and link preview now

Screenshots the current version in a headless browser (the `gpu` preset when ready, else the project default) into `thumb.png` and the 1200 × 630 `og.png`, replacing a captured poster. Renders also run on their own when a version goes live.

Responses
  • 200Rendered
  • 404Not found
  • 409No live version yet
  • 502The browser could not render the page
post/api/v1/assets/{assetId}/thumbnailkey: write

Upload a poster image

Raw `image/png` body up to 4 MiB, typically captured from the viewer.

Request body (image/png)
{
  "type": "string",
  "format": "binary"
}
Responses
  • 200Stored
  • 404Not found
  • 413Too large
  • 415Not a PNG

Variants

get/api/v1/assets/{assetId}/variantskey: read

List variants per version

Responses
  • 200Versions with variants; ready variants with stats carry a `score`
  • 404Not found
post/api/v1/assets/{assetId}/variantskey: write

Request a variant

Provide a named `preset`, an explicit `spec`, or budget mode (`budgetBytes` + `target`): the optimizer searches for the largest spec under the byte budget that the target runtime can load. Spec keys include `palette` (merge texture-less materials into one palette texture) and `ntex` (a separate size cap for normal, occlusion, and metallic-roughness textures). Returns 201 when a job was queued, 200 when the variant already existed.

Request body (application/json)
{
  "type": "object",
  "properties": {
    "versionId": {
      "type": "string"
    },
    "preset": {
      "type": "string"
    },
    "spec": {
      "type": "object",
      "properties": {
        "geo": {
          "type": "string",
          "enum": [
            "meshopt",
            "draco",
            "none",
            "raw"
          ]
        },
        "tex": {
          "type": "string",
          "enum": [
            "ktx2",
            "webp",
            "avif",
            "jpeg",
            "png",
            "original"
          ]
        },
        "maxtex": {
          "anyOf": [
            {
              "type": "number",
              "const": 256
            },
            {
              "type": "number",
              "const": 512
            },
            {
              "type": "number",
              "const": 1024
            },
            {
              "type": "number",
              "const": 2048
            },
            {
              "type": "number",
              "const": 4096
            }
          ]
        },
        "q": {
          "type": "string",
          "enum": [
            "low",
            "med",
            "high"
          ]
        },
        "anim": {
          "type": "string",
          "enum": [
            "keep",
            "strip"
          ]
        },
        "simplify": {
          "type": "number",
          "minimum": 0.1,
          "maximum": 1
        },
        "ntex": {
          "anyOf": [
            {
              "type": "number",
              "const": 256
            },
            {
              "type": "number",
              "const": 512
            },
            {
              "type": "number",
              "const": 1024
            },
            {
              "type": "number",
              "const": 2048
            },
            {
              "type": "number",
              "const": 4096
            }
          ]
        },
        "palette": {
          "type": "boolean"
        }
      },
      "required": [
        "geo",
        "tex",
        "maxtex",
        "q",
        "anim"
      ]
    },
    "budgetBytes": {
      "type": "integer",
      "minimum": 10000,
      "maximum": 2147483648
    },
    "target": {
      "type": "string",
      "enum": [
        "threejs",
        "babylon",
        "model-viewer",
        "playcanvas",
        "unity-gltfast",
        "godot",
        "unreal",
        "filament",
        "lvgl",
        "quick-look"
      ]
    }
  }
}
Responses
  • 200Existing variant
  • 201Created
  • 400Invalid body
  • 404Not found
  • 409No uploaded version
get/api/v1/assets/{assetId}/compatkey: read

Runtime compatibility report per ready variant

Verdicts per runtime (three.js, Babylon.js, model-viewer, PlayCanvas, Unity glTFast, Godot, Unreal, Filament, LVGL, Quick Look): `ok`, `needs-decoder` (with the decoders), or `unsupported` (with the blocking extensions).

Responses
  • 200Versions with variant verdicts
  • 404Not found

Versions

patch/api/v1/assets/{assetId}/versions/{versionId}key: write

Set or clear a version note

The note says what changed in this version; it shows on the dashboard and on share pages next to the change chips.

Request body (application/json)
{
  "type": "object",
  "properties": {
    "note": {
      "anyOf": [
        {
          "type": "string",
          "maxLength": 2000
        },
        {
          "type": "null"
        }
      ]
    }
  },
  "required": [
    "note"
  ]
}
Responses
  • 200Version
  • 400Invalid body
  • 404Not found

Reviews

get/api/v1/assets/{assetId}/versions/{versionId}/reviewskey: read

List review decisions on a version

Newest first. `summary` counts approvals and change requests by each reviewer's current stance (their latest decision); `awaitingApproval` is true while publish-on-approval holds this ready version.

Responses
  • 200Reviews
  • 404Not found
post/api/v1/assets/{assetId}/versions/{versionId}/reviewskey: write

Approve or request changes

Emits `version.approved` or `version.changes_requested`. When the project publishes on approval and the decision comes from someone the mode admits (`owner`: an owner or admin's dashboard session; `any`: anyone, API keys included), approving the newest ready version makes it live and emits `asset.ready`; `promoted` says whether that happened. Key-authored decisions show the key name as the reviewer.

Request body (application/json)
{
  "type": "object",
  "properties": {
    "decision": {
      "type": "string",
      "enum": [
        "approved",
        "changes_requested"
      ]
    },
    "note": {
      "anyOf": [
        {
          "type": "string",
          "maxLength": 2000
        },
        {
          "type": "null"
        }
      ]
    }
  },
  "required": [
    "decision"
  ]
}
Responses
  • 201Recorded
  • 400Invalid body
  • 404Not found
  • 409Version still uploading

Comments

get/api/v1/assets/{assetId}/commentskey: read

List review comments on an asset

Chronological; replies carry `parentId`. `anchor` holds the pinned surface point, normal, and camera.

Responses
  • 200Comments
  • 404Not found
post/api/v1/assets/{assetId}/commentskey: write

Post a comment or reply

Key-authored comments show the key name as the author. Emits `comment.created` to webhooks.

Request body (application/json)
{
  "type": "object",
  "properties": {
    "body": {
      "type": "string",
      "minLength": 1,
      "maxLength": 4000
    },
    "parentId": {
      "anyOf": [
        {
          "type": "string",
          "minLength": 1,
          "maxLength": 64
        },
        {
          "type": "null"
        }
      ]
    },
    "anchor": {
      "anyOf": [
        {
          "type": "object",
          "properties": {
            "position": {
              "type": "array",
              "prefixItems": [
                {
                  "type": "number"
                },
                {
                  "type": "number"
                },
                {
                  "type": "number"
                }
              ],
              "items": false,
              "minItems": 3,
              "maxItems": 3
            },
            "normal": {
              "type": "array",
              "prefixItems": [
                {
                  "type": "number"
                },
                {
                  "type": "number"
                },
                {
                  "type": "number"
                }
              ],
              "items": false,
              "minItems": 3,
              "maxItems": 3
            },
            "camera": {
              "type": "object",
              "properties": {
                "yaw": {
                  "type": "number",
                  "minimum": -3600,
                  "maximum": 3600
                },
                "pitch": {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 180
                },
                "distance": {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 1000000
                },
                "fov": {
                  "type": "number",
                  "minimum": 1,
                  "maximum": 179
                }
              },
              "required": [
                "yaw",
                "pitch",
                "distance"
              ]
            },
            "measure": {
              "type": "object",
              "properties": {
                "a": {
                  "type": "array",
                  "prefixItems": [
                    {
                      "type": "number"
                    },
                    {
                      "type": "number"
                    },
                    {
                      "type": "number"
                    }
                  ],
                  "items": false,
                  "minItems": 3,
                  "maxItems": 3
                },
                "b": {
                  "type": "array",
                  "prefixItems": [
                    {
                      "type": "number"
                    },
                    {
                      "type": "number"
                    },
                    {
                      "type": "number"
                    }
                  ],
                  "items": false,
                  "minItems": 3,
                  "maxItems": 3
                }
              },
              "required": [
                "a",
                "b"
              ]
            }
          },
          "required": [
            "position",
            "normal"
          ]
        },
        {
          "type": "null"
        }
      ]
    },
    "versionId": {
      "anyOf": [
        {
          "type": "string",
          "minLength": 1,
          "maxLength": 64
        },
        {
          "type": "null"
        }
      ]
    }
  },
  "required": [
    "body"
  ]
}
Responses
  • 201Created
  • 400Invalid body
  • 404Not found
patch/api/v1/comments/{commentId}key: write

Resolve or reopen a comment

Request body (application/json)
{
  "type": "object",
  "properties": {
    "resolved": {
      "type": "boolean"
    }
  },
  "required": [
    "resolved"
  ]
}
Responses
  • 200Updated
  • 400Invalid body
  • 404Not found
delete/api/v1/comments/{commentId}key: write

Delete a comment (and its replies)

Responses
  • 204Deleted
  • 404Not found

Exports

get/api/v1/assets/{assetId}/exportskey: read

List exports

Responses
  • 200Exports
post/api/v1/assets/{assetId}/exportskey: write

Export a ready variant as an unpacked glTF zip, a C-array header, or a Quick Look USDZ

Served from the CDN at `/{assetId}.zip`, `/{assetId}.h`, or `/{assetId}.usdz` with the variant's query string once ready. `usdz` needs a variant with JPEG or PNG textures (400 for KTX2); projects with `autoUsdz` get it for every version's compat variant without asking.

Request body (application/json)
{
  "type": "object",
  "properties": {
    "variantId": {
      "type": "string",
      "minLength": 1
    },
    "format": {
      "type": "string",
      "enum": [
        "gltf-zip",
        "c-array",
        "usdz"
      ]
    }
  },
  "required": [
    "variantId",
    "format"
  ]
}
Responses
  • 200Existing export
  • 201Created
  • 400Invalid body, or usdz requested for a KTX2 variant
  • 404Not found
  • 409Variant not ready

Environments

get/api/v1/environmentskey: read

List project environments

Responses
  • 200Environments
post/api/v1/environmentskey: write

Upload an equirect environment (.hdr, .jpg, .png up to 8 MiB)

Multipart form with `name` and `file`. Renders 64/128/256/512/1024 px JPEG maps (plus HDR for HDR sources), served at `/env/{envId}.jpg?size=` and `/env/{envId}.hdr?size=`.

Request body (multipart/form-data)
{
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 80
    },
    "file": {
      "type": "string",
      "description": "binary .hdr, .jpg, or .png up to 8 MiB"
    }
  },
  "required": [
    "name",
    "file"
  ]
}
Responses
  • 201Created
  • 400Invalid body
  • 413Too large
  • 415Unsupported image
get/api/v1/environments/{envId}key: read

Get an environment

Responses
  • 200Environment
  • 404Not found
delete/api/v1/environments/{envId}key: write

Delete an environment

Responses
  • 200Deleted
  • 404Not found

Delivery

post/api/v1/assets/{assetId}/signkey: write

Mint a signed CDN URL

Request body (application/json)
{
  "type": "object",
  "properties": {
    "preset": {
      "type": "string"
    },
    "spec": {
      "type": "object",
      "properties": {
        "geo": {
          "type": "string",
          "enum": [
            "meshopt",
            "draco",
            "none",
            "raw"
          ]
        },
        "tex": {
          "type": "string",
          "enum": [
            "ktx2",
            "webp",
            "avif",
            "jpeg",
            "png",
            "original"
          ]
        },
        "maxtex": {
          "anyOf": [
            {
              "type": "number",
              "const": 256
            },
            {
              "type": "number",
              "const": 512
            },
            {
              "type": "number",
              "const": 1024
            },
            {
              "type": "number",
              "const": 2048
            },
            {
              "type": "number",
              "const": 4096
            }
          ]
        },
        "q": {
          "type": "string",
          "enum": [
            "low",
            "med",
            "high"
          ]
        },
        "anim": {
          "type": "string",
          "enum": [
            "keep",
            "strip"
          ]
        },
        "simplify": {
          "type": "number",
          "minimum": 0.1,
          "maximum": 1
        },
        "ntex": {
          "anyOf": [
            {
              "type": "number",
              "const": 256
            },
            {
              "type": "number",
              "const": 512
            },
            {
              "type": "number",
              "const": 1024
            },
            {
              "type": "number",
              "const": 2048
            },
            {
              "type": "number",
              "const": 4096
            }
          ]
        },
        "palette": {
          "type": "boolean"
        }
      },
      "required": [
        "geo",
        "tex",
        "maxtex",
        "q",
        "anim"
      ]
    },
    "versionId": {
      "type": "string"
    },
    "ttlSeconds": {
      "type": "integer",
      "minimum": 60,
      "maximum": 604800
    },
    "dl": {
      "type": "boolean"
    }
  }
}
Responses
  • 200Signed URL
  • 400Invalid body
  • 404Not found

Collections

get/api/v1/collectionskey: read

List collections

Responses
  • 200Collections with asset counts
post/api/v1/collectionskey: write

Create a collection

Request body (application/json)
{
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 120
    },
    "description": {
      "anyOf": [
        {
          "type": "string",
          "maxLength": 1000
        },
        {
          "type": "null"
        }
      ]
    }
  },
  "required": [
    "name"
  ]
}
Responses
  • 201Created
  • 400Invalid body
get/api/v1/collections/{collectionId}key: read

Collection with its assets in order

Responses
  • 200Collection detail
  • 404Not found
patch/api/v1/collections/{collectionId}key: write

Rename a collection or replace its membership

`assetIds` replaces the membership in the given order (max 50; ids outside the project are dropped).

Request body (application/json)
{
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 120
    },
    "description": {
      "anyOf": [
        {
          "type": "string",
          "maxLength": 1000
        },
        {
          "type": "null"
        }
      ]
    },
    "assetIds": {
      "maxItems": 50,
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1,
        "maxLength": 64
      }
    }
  }
}
Responses
  • 200Updated
  • 400Invalid body
  • 404Not found
delete/api/v1/collections/{collectionId}key: write

Delete a collection (its share links stop resolving)

Responses
  • 200Deleted
  • 404Not found

Billing

post/api/v1/billing/checkoutdashboard only

Start a hosted Stripe Checkout for a paid plan

Dashboard session with the owner role. Returns the Checkout URL to redirect to.

Request body (application/json)
{
  "type": "object",
  "properties": {
    "plan": {
      "type": "string",
      "enum": [
        "pro",
        "business"
      ]
    },
    "interval": {
      "default": "month",
      "type": "string",
      "enum": [
        "month",
        "year"
      ]
    }
  },
  "required": [
    "plan"
  ]
}
Responses
  • 200Checkout URL
  • 400Invalid body
  • 403Not an owner
  • 409Billing not configured
post/api/v1/billing/portaldashboard only

Open the Stripe Customer Portal

Dashboard session with the owner role; needs an existing subscription.

Responses
  • 200Portal URL
  • 403Not an owner
  • 409No subscription or billing not configured
post/api/v1/billing/switch-intervaldashboard only

Switch the plan subscription between monthly and yearly billing

Dashboard session with the owner role; needs an active paid subscription. The cycle restarts now and is invoiced immediately with the unused remainder credited. Yearly billing keeps the metered overage prices on a second, monthly subscription.

Request body (application/json)
{
  "type": "object",
  "properties": {
    "interval": {
      "type": "string",
      "enum": [
        "month",
        "year"
      ]
    }
  },
  "required": [
    "interval"
  ]
}
Responses
  • 200The new interval and the plan
  • 400Invalid body
  • 403Not an owner
  • 409No subscription, already on that interval, or billing not configured

Account

get/api/v1/mekey: read

Who the key or session is

The org and its plan, the project requests work in (a key pinned to a project, the `X-GLBoost-Project` header, or the oldest project), the member role (null for keys), and the key name and scopes. The SDK defaults `projectId` from it; `glboost whoami` prints it.

Responses
  • 200Caller identity

Usage

get/api/v1/usagekey: read

Usage for the project

Query: `from`, `to` (YYYY-MM-DD, default last 30 days). Returns rolled-up days (`daily`), live delivery stats for the last 7 days (`delivery`, zeros when Analytics Engine is not configured), and share activity (`shares`).

Responses
  • 200Usage summary

Shares

get/api/v1/shareskey: read

List share links for an asset or collection

Query: `assetId` or `collectionId` (one is required).

Responses
  • 200Shares
  • 400Invalid body
post/api/v1/shareskey: write

Create a share link

`allowComments` lets viewers leave review comments; `allowReview` (defaults to `allowComments`) lets them approve or request changes on the version they see.

Request body (application/json)
{
  "type": "object",
  "properties": {
    "accessMode": {
      "type": "string",
      "enum": [
        "link",
        "password",
        "email",
        "org"
      ]
    },
    "password": {
      "anyOf": [
        {
          "type": "string",
          "minLength": 4,
          "maxLength": 200
        },
        {
          "type": "null"
        }
      ]
    },
    "allowedEmails": {
      "maxItems": 200,
      "type": "array",
      "items": {
        "type": "string",
        "maxLength": 200,
        "format": "email",
        "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
      }
    },
    "allowedDomains": {
      "maxItems": 50,
      "type": "array",
      "items": {
        "type": "string",
        "maxLength": 200,
        "pattern": "^@?[a-z0-9.-]+\\.[a-z]{2,}$"
      }
    },
    "allowDownload": {
      "type": "string",
      "enum": [
        "none",
        "optimized",
        "all"
      ]
    },
    "watermark": {
      "type": "boolean"
    },
    "showStats": {
      "type": "boolean"
    },
    "allowComments": {
      "type": "boolean"
    },
    "allowReview": {
      "type": "boolean"
    },
    "expiresAt": {
      "anyOf": [
        {},
        {
          "type": "null"
        }
      ]
    },
    "maxViews": {
      "anyOf": [
        {
          "type": "integer",
          "exclusiveMinimum": 0,
          "maximum": 1000000
        },
        {
          "type": "null"
        }
      ]
    },
    "notifyOnView": {
      "type": "boolean"
    },
    "pinnedVersionId": {
      "type": [
        "string",
        "null"
      ]
    },
    "assetId": {
      "type": "string",
      "minLength": 1
    },
    "collectionId": {
      "type": "string",
      "minLength": 1
    }
  },
  "required": [
    "accessMode"
  ]
}
Responses
  • 201Created
  • 400Invalid body
  • 404Not found
get/api/v1/shares/{shareId}key: read

Get a share link

Responses
  • 200Share
  • 404Not found
patch/api/v1/shares/{shareId}key: write

Update a share link

Request body (application/json)
{
  "type": "object",
  "properties": {
    "accessMode": {
      "type": "string",
      "enum": [
        "link",
        "password",
        "email",
        "org"
      ]
    },
    "password": {
      "anyOf": [
        {
          "type": "string",
          "minLength": 4,
          "maxLength": 200
        },
        {
          "type": "null"
        }
      ]
    },
    "allowedEmails": {
      "maxItems": 200,
      "type": "array",
      "items": {
        "type": "string",
        "maxLength": 200,
        "format": "email",
        "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
      }
    },
    "allowedDomains": {
      "maxItems": 50,
      "type": "array",
      "items": {
        "type": "string",
        "maxLength": 200,
        "pattern": "^@?[a-z0-9.-]+\\.[a-z]{2,}$"
      }
    },
    "allowDownload": {
      "type": "string",
      "enum": [
        "none",
        "optimized",
        "all"
      ]
    },
    "watermark": {
      "type": "boolean"
    },
    "showStats": {
      "type": "boolean"
    },
    "allowComments": {
      "type": "boolean"
    },
    "allowReview": {
      "type": "boolean"
    },
    "expiresAt": {
      "anyOf": [
        {},
        {
          "type": "null"
        }
      ]
    },
    "maxViews": {
      "anyOf": [
        {
          "type": "integer",
          "exclusiveMinimum": 0,
          "maximum": 1000000
        },
        {
          "type": "null"
        }
      ]
    },
    "notifyOnView": {
      "type": "boolean"
    },
    "pinnedVersionId": {
      "type": [
        "string",
        "null"
      ]
    }
  }
}
Responses
  • 200Share
  • 400Invalid body
  • 404Not found
delete/api/v1/shares/{shareId}key: write

Revoke a share link

Responses
  • 200Revoked
  • 404Not found
get/api/v1/shares/{shareId}/eventskey: read

Audit log: sessions with their events

Responses
  • 200Sessions
  • 404Not found

API keys

get/api/v1/keysdashboard only

List API keys

Responses
  • 200Keys (hashes omitted)
post/api/v1/keysdashboard only

Create an API key

The plain key is returned once in `key`; only its hash is stored.

Request body (application/json)
{
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 80
    },
    "scopes": {
      "minItems": 1,
      "maxItems": 2,
      "type": "array",
      "items": {
        "type": "string",
        "enum": [
          "read",
          "write"
        ]
      }
    },
    "projectId": {
      "type": "string",
      "minLength": 1
    }
  },
  "required": [
    "name",
    "scopes"
  ]
}
Responses
  • 201Created
  • 400Invalid body
  • 404Not found
delete/api/v1/keys/{keyId}dashboard only

Revoke an API key

Responses
  • 200Revoked
  • 404Not found

Members

get/api/v1/membersdashboard only

List members and open invitations

Invitation links are included for owners and admins only.

Responses
  • 200Members, invitations, and seats used against the plan
patch/api/v1/members/{userId}dashboard only

Change a member's role

Owners and admins may set roles no higher than their own, never their own role; the last owner cannot be demoted.

Request body (application/json)
{
  "type": "object",
  "properties": {
    "role": {
      "type": "string",
      "enum": [
        "owner",
        "admin",
        "member"
      ]
    }
  },
  "required": [
    "role"
  ]
}
Responses
  • 200Updated
  • 400Invalid body
  • 403Not allowed for your role
  • 404Not found
  • 409Would leave the organisation without an owner
delete/api/v1/members/{userId}dashboard only

Remove a member (or leave)

Anyone may remove themselves except the last owner.

Responses
  • 200Removed
  • 403Not allowed for your role
  • 404Not found
  • 409Would leave the organisation without an owner
post/api/v1/members/invitesdashboard only

Invite someone by email

Owners and admins only; admins cannot grant the owner role. Counts against the plan's seats (402 when full). An open invitation for the same address is replaced, so this doubles as "resend". The invitation is emailed when email is configured; `email.sent` says whether it was.

Request body (application/json)
{
  "type": "object",
  "properties": {
    "email": {
      "type": "string",
      "maxLength": 200,
      "format": "email",
      "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
    },
    "role": {
      "default": "member",
      "type": "string",
      "enum": [
        "owner",
        "admin",
        "member"
      ]
    }
  },
  "required": [
    "email"
  ]
}
Responses
  • 201Created
  • 400Invalid body
  • 402Plan has no free seat
  • 409Already a member
delete/api/v1/members/invites/{inviteId}dashboard only

Revoke an invitation

Responses
  • 200Revoked
  • 404Not found

Webhooks

get/api/v1/webhooksdashboard only

List webhooks

Responses
  • 200Webhooks (secrets omitted)
post/api/v1/webhooksdashboard only

Create a webhook

The signing secret is returned once in `secret`.

Request body (application/json)
{
  "type": "object",
  "properties": {
    "url": {
      "type": "string",
      "maxLength": 500
    },
    "events": {
      "minItems": 1,
      "type": "array",
      "items": {
        "type": "string",
        "enum": [
          "asset.ready",
          "variant.ready",
          "variant.failed",
          "share.viewed",
          "share.downloaded",
          "comment.created",
          "version.approved",
          "version.changes_requested",
          "usage.threshold"
        ]
      }
    }
  },
  "required": [
    "url",
    "events"
  ]
}
Responses
  • 201Created
  • 400Invalid body
patch/api/v1/webhooks/{webhookId}dashboard only

Update a webhook

Request body (application/json)
{
  "type": "object",
  "properties": {
    "active": {
      "type": "boolean"
    },
    "url": {
      "type": "string",
      "maxLength": 500
    },
    "events": {
      "minItems": 1,
      "type": "array",
      "items": {
        "type": "string",
        "enum": [
          "asset.ready",
          "variant.ready",
          "variant.failed",
          "share.viewed",
          "share.downloaded",
          "comment.created",
          "version.approved",
          "version.changes_requested",
          "usage.threshold"
        ]
      }
    }
  }
}
Responses
  • 200Webhook
  • 400Invalid body
  • 404Not found
delete/api/v1/webhooks/{webhookId}dashboard only

Delete a webhook

Responses
  • 200Deleted
  • 404Not found
get/api/v1/webhooks/{webhookId}/deliveriesdashboard only

Recent deliveries

Query: `limit` (1–100, default 20).

Responses
  • 200Deliveries
  • 404Not found