# Webhooks

## 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

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

### Create/Update Webhook

**Endpoint:**

```
PUT https://api.glossi.app/api/v1/webhooks
```

**Headers:**

| Header         | Value              |
| -------------- | ------------------ |
| `X-API-Key`    | Your API key       |
| `Content-Type` | `application/json` |

**Body:**

{% code title="Body (JSON)" %}

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

{% endcode %}

**Response:**

{% code title="Response (JSON)" %}

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

{% endcode %}

{% hint style="warning" %}
Important: Save the `secret` value — you'll need it to verify webhook signatures. This is only shown once when creating or updating the webhook.
{% endhint %}

### Get Current Webhook

**Endpoint:**

```
GET https://api.glossi.app/api/v1/webhooks
```

**Headers:**

| Header      | Value        |
| ----------- | ------------ |
| `X-API-Key` | Your API key |

**Response:**

{% code title="Response (JSON)" %}

```json
{
  "id": "webhook-uuid",
  "url": "https://your-app.com/webhooks/glossi",
  "events": ["model.processed", "render.complete"],
  "enabled": true,
  "description": "Production webhook",
  "lastTriggeredAt": "2025-01-15T10:30:00.000Z",
  "lastResponseStatus": 200,
  "failureCount": 0
}
```

{% endcode %}

### Delete Webhook

**Endpoint:**

```
DELETE https://api.glossi.app/api/v1/webhooks
```

**Headers:**

| Header      | Value        |
| ----------- | ------------ |
| `X-API-Key` | Your API key |

## Webhook Payloads

All webhooks follow this format:

{% code title="Generic payload" %}

```json
{
  "event": "event.name",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "data": {
    // Event-specific data
  }
}
```

{% endcode %}

### model.processed

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

{% code title="model.processed" %}

```json
{
  "event": "model.processed",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "data": {
    "modelId": "model-uuid",
    "name": "Chair Model",
    "status": "READY",
    "glbFilePath": "https://s3.../file.glb",
    "thumbnailUrl": "https://s3.../file.jpg"
  }
}
```

{% endcode %}

### model.failed

Sent when model processing fails.

{% code title="model.failed" %}

```json
{
  "event": "model.failed",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "data": {
    "modelId": "model-uuid",
    "name": "Chair Model",
    "status": "FAILED",
    "error": "Model conversion failed: unsupported format"
  }
}
```

{% endcode %}

### project.created

Sent when a project is created.

{% code title="project.created" %}

```json
{
  "event": "project.created",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "data": {
    "projectId": "project-uuid",
    "name": "Chair Project",
    "modelIds": ["model-uuid"],
    "templateId": "template-uuid"
  }
}
```

{% endcode %}

### job.complete

Sent when a Jobs API workflow completes successfully.

{% code title="job.complete" %}

```json
{
  "event": "job.complete",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "data": {
    "jobId": "job-uuid",
    "status": "COMPLETE",
    "results": [
      {
        "model": { "id": "model-uuid", "name": "Chair" },
        "project": { "id": "project-uuid", "name": "Chair Project" }
      }
    ]
  }
}
```

{% endcode %}

### job.failed

Sent when a Jobs API workflow fails.

{% code title="job.failed" %}

```json
{
  "event": "job.failed",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "data": {
    "jobId": "job-uuid",
    "status": "FAILED",
    "error": "Model processing timed out"
  }
}
```

{% endcode %}

### render.complete

Sent when a render job completes successfully.

{% code title="render.complete" %}

```json
{
  "event": "render.complete",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "data": {
    "jobId": "render-job-uuid",
    "status": "COMPLETED",
    "projectId": "project-uuid",
    "renderIds": ["render-uuid-1", "render-uuid-2"]
  }
}
```

{% endcode %}

### render.failed

Sent when a render job fails.

{% code title="render.failed" %}

```json
{
  "event": "render.failed",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "data": {
    "jobId": "render-job-uuid",
    "projectId": "project-uuid",
    "error": "Render timed out"
  }
}
```

{% endcode %}

## 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 hash of the request body using your webhook secret.

### JavaScript/Node.js

{% code title="Node.js verification" %}

```javascript
const crypto = require('crypto');

function verifyWebhook(payload, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

// Express middleware example
app.post('/webhooks/glossi', (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');
  }

  // Process the webhook
  const { event, data } = req.body;
  console.log(`Received ${event}:`, data);

  res.status(200).send('OK');
});
```

{% endcode %}

### Python

{% code title="Python verification" %}

```python
import hmac
import hashlib
import json

def verify_webhook(payload: dict, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(),
        json.dumps(payload).encode(),
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(signature, expected)

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

    if not verify_webhook(request.json, signature, secret):
        return 'Invalid signature', 401

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

    return 'OK', 200
```

{% endcode %}

### PHP

{% code title="PHP verification" %}

```php
function verifyWebhook($payload, $signature, $secret) {
    $expected = hash_hmac('sha256', json_encode($payload), $secret);
    return hash_equals($expected, $signature);
}

// Usage
$signature = $_SERVER['HTTP_X_GLOSSI_SIGNATURE'];
$payload = json_decode(file_get_contents('php://input'), true);
$secret = getenv('GLOSSI_WEBHOOK_SECRET');

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

// Process webhook
$event = $payload['event'];
$data = $payload['data'];
```

{% endcode %}

{% hint style="info" %}
Always verify the signature on incoming webhooks to ensure authenticity and integrity.
{% endhint %}

## Retry Behavior

If your webhook endpoint returns a non-2xx status code, Glossi will retry the delivery:

* Retry attempts: 3
* Retry delay: Exponential backoff (1 min, 5 min, 30 min)
* Timeout: 30 seconds per request

After all retries fail, the `failureCount` on your webhook configuration will increment. You can check this via `GET /api/v1/webhooks`.

## Best Practices

* Always verify signatures — Never trust webhook data without verifying the signature
* Respond quickly — Return a 200 status within 5 seconds; process asynchronously if needed
* Handle duplicates — Webhooks may be delivered more than once; use the `jobId` or event data to deduplicate
* Use HTTPS — Always use HTTPS endpoints in production
* Log everything — Keep logs of received webhooks for debugging

## Testing Webhooks

For local development, use a tunneling service like [ngrok](https://ngrok.com/) to expose your local server:

{% code title="ngrok" %}

```bash
# Start ngrok
ngrok http 3000

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

{% endcode %}

You can also use webhook testing services like [webhook.site](https://webhook.site/) to inspect payloads without writing code.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.glossi.io/glossi-api/webhooks.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
