Webhooks
Webhooks let Quazzar Cloud OS send real-time event notifications to your systems. When something happens on the instance — an app is installed, a backup completes, an alert fires — Cloud OS sends an HTTP POST request to your configured endpoint.
Setup
Creating a Webhook via the API
curl -X POST http://localhost:8080/api/webhooks \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/quazzar",
"events": ["app.installed", "app.crashed", "backup.completed"],
"active": true
}'Returns the webhook configuration with a signing secret:
{
"id": "wh_abc123",
"url": "https://your-app.com/webhooks/quazzar",
"events": ["app.installed", "app.crashed", "backup.completed"],
"secret": "whsec_abc123...",
"active": true,
"created_at": "2025-01-15T10:30:00Z"
}Copy the signing secret immediately. It is only shown once at creation time. You will need it to verify webhook signatures.
Creating a Webhook via the UI
- Open the Cloud OS dashboard.
- Navigate to Settings then Webhooks.
- Click Add Webhook.
- Enter the target URL (HTTPS recommended).
- Select which events to subscribe to or choose “All events”.
- Copy the generated signing secret and store it securely.
- Click Save.
Managing Webhooks
# List all webhooks
curl http://localhost:8080/api/webhooks \
-H "Authorization: Bearer <token>"
# Update a webhook
curl -X PUT http://localhost:8080/api/webhooks/wh_abc123 \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"events": ["app.installed", "app.crashed", "backup.completed", "alert.fired"],
"active": true
}'
# Delete a webhook
curl -X DELETE http://localhost:8080/api/webhooks/wh_abc123 \
-H "Authorization: Bearer <token>"Event Types
| Event | Trigger |
|---|---|
app.installed | A new app is installed from a template |
app.started | An app container is started |
app.stopped | An app container is stopped |
app.crashed | An app container exits unexpectedly |
app.uninstalled | An app is removed from the instance |
app.updated | An app is updated to a new version |
backup.started | A backup operation begins |
backup.completed | A backup finishes successfully |
backup.failed | A backup operation fails |
backup.restored | A restore operation completes |
alert.fired | An alert rule threshold is breached |
alert.resolved | An alert condition returns to normal |
domain.added | A custom domain is configured |
domain.ssl_provisioned | An SSL certificate is issued for a domain |
domain.ssl_expiring | An SSL certificate is nearing expiration |
vpn.peer_added | A new VPN peer is created |
vpn.peer_removed | A VPN peer is deleted |
security.scan_completed | A security scan finishes |
security.finding_critical | A critical security finding is detected |
system.disk_warning | Disk usage exceeds the warning threshold |
system.update_available | A new Cloud OS version is available |
Payload Format
Every webhook request is an HTTP POST with a JSON body:
{
"id": "evt_abc123",
"type": "app.installed",
"timestamp": "2025-01-15T10:30:00Z",
"instance_id": "node_abc123",
"data": {
"app_id": "app_def456",
"name": "my-nextcloud",
"template": "nextcloud",
"status": "running"
}
}| Field | Description |
|---|---|
id | Unique event identifier for idempotency |
type | Event type string (see table above) |
timestamp | ISO 8601 timestamp of when the event occurred |
instance_id | The Cloud OS node ID that generated the event |
data | Event-specific payload containing the relevant resource |
The data field varies by event type and contains the full resource object relevant to the event.
HMAC-SHA256 Signature Verification
Every webhook request includes an X-Quazzar-Signature header containing an HMAC-SHA256 signature of the raw request body, computed using your webhook signing secret.
X-Quazzar-Signature: sha256=a1b2c3d4e5f6...Always verify the signature before processing the payload to prevent forged events.
Verification in Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"strings"
)
func verifySignature(payload []byte, signature string, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(payload)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signature))
}Verification in Python
import hmac
import hashlib
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode("utf-8"),
payload,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(f"sha256={expected}", signature)Verification in Node.js
const crypto = require('crypto');
function verifySignature(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload, 'utf-8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from('sha256=' + expected),
Buffer.from(signature),
);
}Always use constant-time comparison (like hmac.compare_digest in Python or crypto.timingSafeEqual in Node.js) when verifying signatures. Simple string comparison is vulnerable to timing attacks.
Retry Policy
If your endpoint returns a non-2xx status code or the request times out (30-second timeout), Cloud OS retries delivery with exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry | 30 minutes |
| 4th retry | 2 hours |
| 5th retry | 12 hours |
After 5 failed retries, the event is marked as failed. You can view failed deliveries and manually retry them from the Cloud OS dashboard under Settings then Webhooks then Delivery Log.
If a webhook endpoint fails 10 consecutive deliveries, the webhook is automatically deactivated. An alert.fired event with type webhook_disabled is generated (delivered to other active webhooks) so you are notified.
Idempotency
Each event includes a unique id field. Use this to deduplicate events on your end in case of retries. If you receive an event with an id you have already processed, acknowledge it with a 200 response but skip processing.
Example Handler
A minimal Express.js webhook handler:
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.raw({ type: 'application/json' }));
const WEBHOOK_SECRET = process.env.QUAZZAR_WEBHOOK_SECRET;
app.post('/webhooks/quazzar', (req, res) => {
const signature = req.headers['x-quazzar-signature'];
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
if (!crypto.timingSafeEqual(
Buffer.from('sha256=' + expected),
Buffer.from(signature)
)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
console.log('Received event:', event.type, 'id:', event.id);
switch (event.type) {
case 'app.crashed':
// Trigger incident response
break;
case 'backup.completed':
// Update backup tracking system
break;
case 'alert.fired':
// Forward to PagerDuty / Opsgenie
break;
default:
console.log('Unhandled event type:', event.type);
}
res.status(200).send('OK');
});
app.listen(3000);Tips
- Use a message queue (Redis, RabbitMQ, SQS) to process webhook events asynchronously so your handler responds within the 30-second timeout.
- Store the event
idto implement idempotency and safely handle retries. - Subscribe only to the events you need to reduce noise and processing overhead.
- Test your webhook endpoint using the Send Test Event button in the Cloud OS dashboard before relying on it in production.
- Monitor the delivery log periodically for failed events that may need manual retry.