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"?: [...] };issueslists 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. 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
/api/v1/openapi.jsonpublicThis document
200OpenAPI 3.1 document
Projects
/api/v1/projectskey: readList projects
A key pinned to a project sees only that project.
200Projects
/api/v1/projects/{projectId}key: readGet a project
200Project404Not found
/api/v1/projects/{projectId}key: writeUpdate delivery settings
{
"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+)?$"
}
}
}
}200Project400Invalid body404Not found
Organizations
/api/v1/orgs/{orgId}key: writeUpdate organization branding
{
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"brandColor": {
"anyOf": [
{
"type": "string",
"pattern": "^#[0-9a-fA-F]{6}$"
},
{
"type": "null"
}
]
}
}
}200Organization400Invalid body404Not found
Uploads
/api/v1/uploadskey: writeStart 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`.
{
"type": "object",
"properties": {
"projectId": {
"type": "string",
"minLength": 1
},
"filename": {
"type": "string",
"minLength": 1,
"maxLength": 200
},
"contentType": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"bytes": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
},
"assetId": {
"type": "string",
"minLength": 1
}
},
"required": [
"projectId",
"filename",
"contentType",
"bytes"
]
}200Upload target400Invalid body413File too large415Unsupported format
/api/v1/uploads/{versionId}/completekey: writeFinish an upload and queue optimization
200Queued jobs404Not found409Object not yet on the CDN, or the upload was aborted
/api/v1/uploads/{versionId}/abortkey: writeGive 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.
200Version marked failed404Not found409The upload already completed
Assets
/api/v1/assetskey: readList assets
200Assets
/api/v1/assets/{assetId}key: readGet an asset with versions and variants
200Asset detail404Not found
/api/v1/assets/{assetId}key: writeUpdate name, visibility, viewer settings, hotspots
{
"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"
}
}
},
{
"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"
]
}
},
"required": [
"id",
"title",
"position",
"normal"
]
}
}
}
}200Asset400Invalid body404Not found
/api/v1/assets/{assetId}key: writeDelete an asset
200Deleted404Not found
/api/v1/assets/{assetId}/thumbnailkey: writeUpload a poster image
Raw `image/png` body up to 4 MiB, typically captured from the viewer.
{
"type": "string",
"format": "binary"
}200Stored404Not found413Too large415Not a PNG
Variants
/api/v1/assets/{assetId}/variantskey: readList variants per version
200Versions with variants404Not found
/api/v1/assets/{assetId}/variantskey: writeRequest 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. Returns 201 when a job was queued, 200 when the variant already existed.
{
"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
}
},
"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"
]
}
}
}200Existing variant201Created400Invalid body404Not found409No uploaded version
/api/v1/assets/{assetId}/compatkey: readRuntime 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).
200Versions with variant verdicts404Not found
Comments
/api/v1/assets/{assetId}/commentskey: readList review comments on an asset
Chronological; replies carry `parentId`. `anchor` holds the pinned surface point, normal, and camera.
200Comments404Not found
/api/v1/assets/{assetId}/commentskey: writePost a comment or reply
Key-authored comments show the key name as the author. Emits `comment.created` to webhooks.
{
"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"
]
}
},
"required": [
"position",
"normal"
]
},
{
"type": "null"
}
]
},
"versionId": {
"anyOf": [
{
"type": "string",
"minLength": 1,
"maxLength": 64
},
{
"type": "null"
}
]
}
},
"required": [
"body"
]
}201Created400Invalid body404Not found
/api/v1/comments/{commentId}key: writeResolve or reopen a comment
{
"type": "object",
"properties": {
"resolved": {
"type": "boolean"
}
},
"required": [
"resolved"
]
}200Updated400Invalid body404Not found
/api/v1/comments/{commentId}key: writeDelete a comment (and its replies)
204Deleted404Not found
Exports
/api/v1/assets/{assetId}/exportskey: readList exports
200Exports
/api/v1/assets/{assetId}/exportskey: writeExport a ready variant as an unpacked glTF zip or a C-array header
Served from the CDN at `/{assetId}.zip` or `/{assetId}.h` with the variant's query string once ready.
{
"type": "object",
"properties": {
"variantId": {
"type": "string",
"minLength": 1
},
"format": {
"type": "string",
"enum": [
"gltf-zip",
"c-array"
]
}
},
"required": [
"variantId",
"format"
]
}200Existing export201Created400Invalid body404Not found409Variant not ready
Environments
/api/v1/environmentskey: readList project environments
200Environments
/api/v1/environmentskey: writeUpload 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=`.
{
"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"
]
}201Created400Invalid body413Too large415Unsupported image
/api/v1/environments/{envId}key: readGet an environment
200Environment404Not found
/api/v1/environments/{envId}key: writeDelete an environment
200Deleted404Not found
Delivery
/api/v1/assets/{assetId}/signkey: writeMint a signed CDN URL
{
"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
}
},
"required": [
"geo",
"tex",
"maxtex",
"q",
"anim"
]
},
"versionId": {
"type": "string"
},
"ttlSeconds": {
"type": "integer",
"minimum": 60,
"maximum": 604800
},
"dl": {
"type": "boolean"
}
}
}200Signed URL400Invalid body404Not found
Collections
/api/v1/collectionskey: readList collections
200Collections with asset counts
/api/v1/collectionskey: writeCreate a collection
{
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
"maxLength": 120
},
"description": {
"anyOf": [
{
"type": "string",
"maxLength": 1000
},
{
"type": "null"
}
]
}
},
"required": [
"name"
]
}201Created400Invalid body
/api/v1/collections/{collectionId}key: readCollection with its assets in order
200Collection detail404Not found
/api/v1/collections/{collectionId}key: writeRename a collection or replace its membership
`assetIds` replaces the membership in the given order (max 50; ids outside the project are dropped).
{
"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
}
}
}
}200Updated400Invalid body404Not found
/api/v1/collections/{collectionId}key: writeDelete a collection (its share links stop resolving)
200Deleted404Not found
Billing
/api/v1/billing/checkoutdashboard onlyStart a hosted Stripe Checkout for a paid plan
Dashboard session only. Returns the Checkout URL to redirect to.
{
"type": "object",
"properties": {
"plan": {
"type": "string",
"enum": [
"pro",
"business"
]
}
},
"required": [
"plan"
]
}200Checkout URL400Invalid body409Billing not configured
/api/v1/billing/portaldashboard onlyOpen the Stripe Customer Portal
Dashboard session only; needs an existing subscription.
200Portal URL409No subscription or billing not configured
Usage
/api/v1/usagekey: readUsage 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`).
200Usage summary
Shares
/api/v1/shareskey: readList share links for an asset or collection
Query: `assetId` or `collectionId` (one is required).
200Shares400Invalid body
/api/v1/shareskey: writeCreate a share link
{
"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"
},
"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"
]
}201Created400Invalid body404Not found
/api/v1/shares/{shareId}key: readGet a share link
200Share404Not found
/api/v1/shares/{shareId}key: writeUpdate a share link
{
"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"
},
"expiresAt": {
"anyOf": [
{},
{
"type": "null"
}
]
},
"maxViews": {
"anyOf": [
{
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 1000000
},
{
"type": "null"
}
]
},
"notifyOnView": {
"type": "boolean"
},
"pinnedVersionId": {
"type": [
"string",
"null"
]
}
}
}200Share400Invalid body404Not found
/api/v1/shares/{shareId}key: writeRevoke a share link
200Revoked404Not found
/api/v1/shares/{shareId}/eventskey: readAudit log: sessions with their events
200Sessions404Not found
API keys
/api/v1/keysdashboard onlyList API keys
200Keys (hashes omitted)
/api/v1/keysdashboard onlyCreate an API key
The plain key is returned once in `key`; only its hash is stored.
{
"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"
]
}201Created400Invalid body404Not found
/api/v1/keys/{keyId}dashboard onlyRevoke an API key
200Revoked404Not found
Webhooks
/api/v1/webhooksdashboard onlyList webhooks
200Webhooks (secrets omitted)
/api/v1/webhooksdashboard onlyCreate a webhook
The signing secret is returned once in `secret`.
{
"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"
]
}
}
},
"required": [
"url",
"events"
]
}201Created400Invalid body
/api/v1/webhooks/{webhookId}dashboard onlyUpdate a webhook
{
"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"
]
}
}
}
}200Webhook400Invalid body404Not found
/api/v1/webhooks/{webhookId}dashboard onlyDelete a webhook
200Deleted404Not found
/api/v1/webhooks/{webhookId}/deliveriesdashboard onlyRecent deliveries
Query: `limit` (1–100, default 20).
200Deliveries404Not found