GlossiDocs
Open Glossi

Receive signed webhooks

Webhooks notify your system when supported Glossi state changes. Use them to reduce polling latency, then retrieve current state from the API before making an irreversible downstream decision.

Current webhook delivery is fire-and-forget and does not have a durable retry history. Build consumers that tolerate missed and duplicate events.


Available Events

EventDescription
model.processedModel upload processing completed successfully
model.failedModel processing failed
project.createdA project was created
job.completeA job workflow completed successfully
job.failedA job workflow failed
render.completeA render completed successfully
render.failedA render failed

Configure Your Webhook

You can create or update your webhook configuration using the API.

Create/Update Webhook

Endpoint:

PUT https://api.glossi.io/api/v1/webhooks

Headers:

HeaderValue
X-API-KeyYour API key
Content-Typeapplication/json

Body:

{
  "url": "https://your-app.com/webhooks/glossi",
  "events": [
    "model.processed",
    "model.failed",
    "project.created",
    "job.complete",
    "job.failed",
    "render.complete",
    "render.failed"
  ],
  "enabled": true,
  "description": "Production webhook for n8n"
}

Response:

{
  "id": "webhook-uuid",
  "url": "https://your-app.com/webhooks/glossi",
  "secret": "your-webhook-secret",
  "events": ["model.processed", "model.failed", "project.created", "job.complete", "job.failed", "render.complete", "render.failed"],
  "enabled": true,
  "description": "Production webhook for n8n"
}

Important Save the secret value - you'll need it to verify webhook signatures. If you lose it, you can retrieve it again with GET /api/v1/webhooks/secret, or rotate it with POST /api/v1/webhooks/regenerate-secret. Updating the webhook config does not change the secret.

Note If you omit events, the webhook is subscribed to render.complete and render.failed only. Pass the full list explicitly if you want model, project, and job events too.

Get Current Webhook

Endpoint:

GET https://api.glossi.io/api/v1/webhooks

Headers:

HeaderValue
X-API-KeyYour API key

Response:

{
  "configured": true,
  "id": "webhook-uuid",
  "url": "https://your-app.com/webhooks/glossi",
  "events": ["model.processed", "render.complete"],
  "enabled": true,
  "description": "Production webhook",
  "lastTriggeredAt": "2026-07-28T10:30:00.000Z",
  "failureCount": 0
}

If no webhook is configured, the response is simply { "configured": false }.

Delete Webhook

Endpoint:

DELETE https://api.glossi.io/api/v1/webhooks

Headers:

HeaderValue
X-API-KeyYour API key

Webhook Payloads

All webhooks follow this format:

{
  "event": "event.name",
  "timestamp": "2026-07-28T10:30:00.000Z",
  "data": {
    // Event-specific data
  }
}

Each delivery includes these headers:

HeaderValue
Content-Typeapplication/json
X-Glossi-SignatureHMAC-SHA256 hex digest of the request body (see verification)
X-Glossi-EventThe event name, e.g. render.complete
X-Glossi-TimestampISO 8601 timestamp, same value as timestamp in the payload

model.processed

Sent when a model finishes processing and is ready to use.

{
  "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"
  }
}

model.failed

Sent when model processing fails.

{
  "event": "model.failed",
  "timestamp": "2026-07-28T10:30:00.000Z",
  "data": {
    "modelId": "model-uuid",
    "name": "Chair Model",
    "status": "FAILED",
    "error": "Model conversion failed: unsupported format"
  }
}

project.created

Sent when a project is created. Note that the project fields are nested under a project key.

{
  "event": "project.created",
  "timestamp": "2026-07-28T10:30:00.000Z",
  "data": {
    "project": {
      "projectId": "project-uuid",
      "name": "chair.glb",
      "modelIds": ["model-uuid"],
      "templateId": "template-uuid"
    }
  }
}

job.complete

Sent when a Jobs API workflow completes successfully.

{
  "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" }
      }
    ]
  }
}

job.failed

Sent when a Jobs API workflow fails.

{
  "event": "job.failed",
  "timestamp": "2026-07-28T10:30:00.000Z",
  "data": {
    "jobId": "job-uuid",
    "status": "FAILED",
    "error": "Model processing timed out"
  }
}

render.complete

Sent when a render job completes successfully.

{
  "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"]
  }
}

Note Renders started from the Glossi Studio UI (rather than the API) also trigger this event, with a different data shape: { "renderId": "...", "projectId": "...", "render": { "id", "name", "imageUrl", "fileType", "isVideo", "isPreview" } } - no jobId or renderIds. If your workspace uses both the Studio and the API, handle both shapes (branching on the presence of jobId works well).

render.failed

Sent when a render job fails.

{
  "event": "render.failed",
  "timestamp": "2026-07-28T10:30:00.000Z",
  "data": {
    "jobId": "render-job-uuid",
    "projectId": "project-uuid",
    "error": "Render timed out"
  }
}

Verify Webhook Signatures

All webhook requests include an X-Glossi-Signature header. Always verify this signature to ensure the request came from Glossi and hasn't been tampered with.

The signature is an HMAC-SHA256 hex digest of the raw request body using your webhook secret.

Important Compute the HMAC over the exact bytes you received - never over a re-serialized copy of the parsed JSON. Different JSON encoders produce different byte output (Python's json.dumps inserts spaces, PHP's json_encode escapes slashes), so re-encoding will produce a digest that never matches. Configure your framework to give you the raw body.

JavaScript/Node.js

const crypto = require("crypto")
const express = require("express")

const app = express()

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)
}

// Use express.raw() on the webhook route so req.body is the raw bytes,
// not parsed JSON
app.post("/webhooks/glossi", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.headers["x-glossi-signature"]
  const secret = process.env.GLOSSI_WEBHOOK_SECRET

  if (!verifyWebhook(req.body, signature, secret)) {
    return res.status(401).send("Invalid signature")
  }

  // Parse only after verification
  const { event, data } = JSON.parse(req.body.toString("utf8"))
  console.log(`Received ${event}:`, data)

  res.status(200).send("OK")
})

Python

import hmac
import hashlib
import os

from flask import Flask, request

app = Flask(__name__)

def verify_webhook(raw_body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature or "", expected)

@app.route("/webhooks/glossi", methods=["POST"])
def handle_webhook():
    signature = request.headers.get("X-Glossi-Signature")
    secret = os.environ["GLOSSI_WEBHOOK_SECRET"]

    # request.get_data() is the raw request body - verify against that
    if not verify_webhook(request.get_data(), signature, secret):
        return "Invalid signature", 401

    payload = request.get_json()
    print(f"Received {payload['event']}: {payload['data']}")

    return "OK", 200

PHP

function verifyWebhook($rawBody, $signature, $secret) {
    $expected = hash_hmac('sha256', $rawBody, $secret);
    return hash_equals($expected, $signature ?? '');
}

// php://input is the raw request body - verify against it before decoding
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_GLOSSI_SIGNATURE'] ?? '';
$secret = getenv('GLOSSI_WEBHOOK_SECRET');

if (!verifyWebhook($rawBody, $signature, $secret)) {
    http_response_code(401);
    exit('Invalid signature');
}

// Decode only after verification
$payload = json_decode($rawBody, true);
$event = $payload['event'];
$data = $payload['data'];

Delivery Behavior

Webhook delivery is not guaranteed and should not be treated as exactly once. Specifics worth designing around:

  • No retries. Each event is delivered with a single POST. If your endpoint is down or times out, that event is not re-sent.
  • 10-second timeout. Your endpoint must respond within 10 seconds or the delivery counts as failed. Acknowledge immediately and process asynchronously.
  • Circuit breaker. After 10 consecutive failed deliveries, Glossi stops sending webhooks to your endpoint. A successful delivery resets the counter. If the breaker has tripped, fix your endpoint and call POST /api/v1/webhooks/reset-failures to resume delivery. You can check failureCount via GET /api/v1/webhooks.

Run a periodic reconciliation process for important workflows. Query the current model, job, project, render, or pipeline state and repair any downstream work that did not receive an event.


Best Practices

  1. Always verify signatures - Never trust webhook data without verifying the signature.
  2. Respond quickly - Acknowledge the request, then process it asynchronously.
  3. Handle duplicates - Use stable resource or event data to make downstream work idempotent.
  4. Reconcile missed events - Periodically retrieve current state from the API.
  5. Use HTTPS - Expose only a secure production endpoint.
  6. Keep your own delivery log - Record the signature, event, resource identifier, and processing result.

Testing Webhooks

For local development, use a tunneling service like ngrok to expose your local server:

# Start ngrok
ngrok http 3000

# Use the ngrok URL when configuring your webhook
# https://abc123.ngrok.io/webhooks/glossi

You can also use webhook testing services like webhook.site to inspect payloads without writing code.

On this page