Webhooks let you build event-driven integrations with Avala. Instead of polling the API for changes, register an HTTPS endpoint and Avala will send an HTTP POST whenever a relevant event occurs.
Quick Setup
from avala import Client
client = Client()
# Create a webhook subscription
webhook = client.webhooks.create(
target_url="https://example.com/webhooks/avala",
events=["export.completed", "dataset.created"],
)
print(f"Webhook {webhook.uid} created")
print(f"Secret: {webhook.secret}") # only shown once — store it!
The webhook secret is only returned in the creation response. Store it securely — you’ll need it to verify signatures.
Events
| Event | Trigger |
|---|
dataset.created | A new dataset is created. |
dataset.updated | A dataset’s metadata is modified. |
dataset.deleted | A dataset is deleted. |
export.completed | An export finishes processing and is ready for download. |
export.failed | An export fails to process. |
task.completed | A task’s results are finalized. |
Every webhook delivery is a JSON POST with three custom headers:
| Header | Description |
|---|
X-Avala-Webhook-Signature | HMAC-SHA256 hex digest for payload verification. |
X-Avala-Webhook-Event | The event type (e.g., export.completed). |
X-Avala-Webhook-Delivery | Unique delivery ID (UUID) for idempotency tracking. |
The request body contains the event payload as JSON. The payload structure matches the corresponding API resource.
export.completed / export.failed
{
"uid": "exp_abc123",
"status": "completed",
"download_url": "https://api.avala.ai/api/v1/exports/exp_abc123/download",
"format": "avala_json",
"created_at": "2026-02-21T14:30:00Z"
}
dataset.created / dataset.updated
{
"uid": "ds_abc123",
"name": "Pedestrian Detection v2",
"owner": "acme-ai",
"slug": "pedestrian-detection-v2",
"data_type": "image",
"item_count": 1250,
"created_at": "2026-02-21T10:00:00Z",
"updated_at": "2026-02-21T14:30:00Z"
}
dataset.deleted
{
"uid": "ds_abc123",
"name": "Pedestrian Detection v2",
"owner": "acme-ai",
"slug": "pedestrian-detection-v2"
}
task.completed
{
"uid": "tsk_abc123",
"status": "completed",
"task_type": "box",
"project_uid": "proj_abc123",
"dataset_item_uid": "item_abc123",
"completed_at": "2026-02-21T14:30:00Z"
}
Verifying Signatures
Every delivery is signed with HMAC-SHA256 using the subscription’s secret. Always verify signatures before processing a webhook to ensure the request came from Avala.
The signature is computed over the JSON-serialized payload with deterministic formatting (compact separators, sorted keys):
HMAC-SHA256(secret, json_serialize(payload, separators=(",",":"), sort_keys=True))
Python
import hmac
import hashlib
import json
def verify_webhook(payload: dict, signature: str, secret: str) -> bool:
"""Verify that a webhook payload was sent by Avala."""
payload_bytes = json.dumps(
payload, separators=(",", ":"), sort_keys=True
).encode("utf-8")
expected = hmac.new(
secret.encode("utf-8"),
payload_bytes,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature)
TypeScript
import { createHmac, timingSafeEqual } from "crypto";
function verifyWebhook(
payload: Record<string, unknown>,
signature: string,
secret: string,
): boolean {
const body = JSON.stringify(payload, Object.keys(payload).sort())
.replace(/: /g, ":")
.replace(/, /g, ",");
const expected = createHmac("sha256", secret)
.update(body)
.digest("hex");
return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
Always use constant-time comparison (hmac.compare_digest in Python, timingSafeEqual in Node.js) to prevent timing attacks.
Example: Flask Webhook Handler
A complete Flask application that receives and verifies Avala webhooks:
import hmac
import hashlib
import json
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = "your-webhook-secret" # from the creation response
def verify_signature(payload: dict, signature: str) -> bool:
payload_bytes = json.dumps(
payload, separators=(",", ":"), sort_keys=True
).encode("utf-8")
expected = hmac.new(
WEBHOOK_SECRET.encode("utf-8"),
payload_bytes,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature)
@app.route("/webhooks/avala", methods=["POST"])
def handle_webhook():
signature = request.headers.get("X-Avala-Webhook-Signature", "")
event_type = request.headers.get("X-Avala-Webhook-Event", "")
delivery_id = request.headers.get("X-Avala-Webhook-Delivery", "")
payload = request.json
if not verify_signature(payload, signature):
return jsonify({"error": "Invalid signature"}), 401
if event_type == "export.completed":
print(f"Export ready: {payload['download_url']}")
elif event_type == "dataset.created":
print(f"New dataset: {payload['name']}")
elif event_type == "task.completed":
print(f"Task completed: {payload['uid']}")
# Return 2xx to acknowledge receipt
return jsonify({"received": True}), 200
if __name__ == "__main__":
app.run(port=5000)
Example: Express Webhook Handler
import express from "express";
import { createHmac, timingSafeEqual } from "crypto";
const app = express();
app.use(express.json());
const WEBHOOK_SECRET = "your-webhook-secret";
app.post("/webhooks/avala", (req, res) => {
const signature = req.headers["x-avala-webhook-signature"] as string;
const eventType = req.headers["x-avala-webhook-event"] as string;
// Verify signature
const body = JSON.stringify(req.body, Object.keys(req.body).sort())
.replace(/: /g, ":")
.replace(/, /g, ",");
const expected = createHmac("sha256", WEBHOOK_SECRET)
.update(body)
.digest("hex");
if (!timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
return res.status(401).json({ error: "Invalid signature" });
}
switch (eventType) {
case "export.completed":
console.log(`Export ready: ${req.body.download_url}`);
break;
case "dataset.created":
console.log(`New dataset: ${req.body.name}`);
break;
case "task.completed":
console.log(`Task done: ${req.body.uid}`);
break;
}
res.json({ received: true });
});
app.listen(5000, () => console.log("Listening on port 5000"));
Retry Policy
If your endpoint doesn’t respond with a 2xx status code, Avala retries with exponential backoff:
| Attempt | Delay |
|---|
| 1st retry | ~1 minute |
| 2nd retry | ~2 minutes |
| 3rd retry | ~4 minutes |
| 4th retry | ~8 minutes |
| 5th retry | ~16 minutes |
After 5 failed retries, the delivery is marked as failed. Client errors (4xx) are treated as permanent failures and are not retried.
Delivery Log
Every delivery attempt is recorded. You can inspect delivery history through the API:
# List recent deliveries
curl https://api.avala.ai/api/v1/webhook-deliveries/ \
-H "X-Avala-Api-Key: $AVALA_API_KEY"
Each delivery record includes:
| Field | Description |
|---|
uid | Unique delivery ID. |
event_type | The event that triggered this delivery. |
payload | The JSON payload that was sent. |
status | pending, delivered, or failed. |
response_status | HTTP status code from your endpoint (null if not yet attempted). |
attempts | Number of delivery attempts so far. |
created_at | When the delivery was created. |
Delivered entries are automatically cleaned up after 30 days. Failed entries are retained for 90 days.
Managing Webhooks
API Endpoints
| Method | Endpoint | Description |
|---|
GET | /api/v1/webhooks/ | List your webhook subscriptions. |
POST | /api/v1/webhooks/ | Create a new subscription. |
GET | /api/v1/webhooks/{uid}/ | Get subscription details. |
PUT | /api/v1/webhooks/{uid}/ | Update a subscription. |
DELETE | /api/v1/webhooks/{uid}/ | Delete a subscription. |
POST | /api/v1/webhooks/{uid}/test/ | Send a test ping event. |
GET | /api/v1/webhook-deliveries/ | List delivery history. |
GET | /api/v1/webhook-deliveries/{uid}/ | Get delivery details. |
Create a Subscription
curl -X POST https://api.avala.ai/api/v1/webhooks/ \
-H "X-Avala-Api-Key: $AVALA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"target_url": "https://example.com/webhooks/avala",
"events": ["dataset.created", "export.completed", "task.completed"]
}'
A signing secret is auto-generated if you don’t provide one. You can also pass your own:
{
"target_url": "https://example.com/webhooks/avala",
"events": ["export.completed"],
"secret": "my-custom-secret"
}
Test a Webhook
Send a ping event to verify your endpoint is reachable:
curl -X POST https://api.avala.ai/api/v1/webhooks/{uid}/test/ \
-H "X-Avala-Api-Key: $AVALA_API_KEY"
Testing During Development
-
Use a tunnel like ngrok to expose a local server:
Copy the HTTPS URL and use it as your webhook
target_url.
-
Send a test ping via the API to verify connectivity:
curl -X POST https://api.avala.ai/api/v1/webhooks/{uid}/test/ \
-H "X-Avala-Api-Key: $AVALA_API_KEY"
-
Check the delivery log to inspect payloads, response codes, and timing:
curl https://api.avala.ai/api/v1/webhook-deliveries/ \
-H "X-Avala-Api-Key: $AVALA_API_KEY"
Security
- HTTPS required — Webhook endpoints must use HTTPS.
- SSRF protection — Avala blocks delivery to private IPs, loopback addresses, and cloud metadata endpoints.
- No redirects — Webhook delivery does not follow HTTP redirects.
- Encrypted secrets — Webhook secrets are encrypted at rest and never exposed after creation.
- One subscription per URL — Each organization can have at most one subscription per target URL.