Skip to main content
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

EventTrigger
dataset.createdA new dataset is created.
dataset.updatedA dataset’s metadata is modified.
dataset.deletedA dataset is deleted.
export.completedAn export finishes processing and is ready for download.
export.failedAn export fails to process.
task.completedA task’s results are finalized.

Payload Format

Every webhook delivery is a JSON POST with three custom headers:
HeaderDescription
X-Avala-Webhook-SignatureHMAC-SHA256 hex digest for payload verification.
X-Avala-Webhook-EventThe event type (e.g., export.completed).
X-Avala-Webhook-DeliveryUnique 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:
AttemptDelay
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:
FieldDescription
uidUnique delivery ID.
event_typeThe event that triggered this delivery.
payloadThe JSON payload that was sent.
statuspending, delivered, or failed.
response_statusHTTP status code from your endpoint (null if not yet attempted).
attemptsNumber of delivery attempts so far.
created_atWhen the delivery was created.
Delivered entries are automatically cleaned up after 30 days. Failed entries are retained for 90 days.

Managing Webhooks

API Endpoints

MethodEndpointDescription
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

  1. Use a tunnel like ngrok to expose a local server:
    ngrok http 5000
    
    Copy the HTTPS URL and use it as your webhook target_url.
  2. 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"
    
  3. 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.