> ## Documentation Index
> Fetch the complete documentation index at: https://avala.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> API reference for webhook subscriptions and deliveries

Create and manage webhook subscriptions to receive real-time notifications when events occur in your Avala organization.

<Tip>
  Webhooks deliver event payloads to your HTTPS endpoint via `POST` requests. Each delivery is signed with your webhook secret so you can verify authenticity. Failed deliveries are retried automatically.
</Tip>

## List Webhooks

```
GET /api/v1/webhooks/
```

Returns all webhook subscriptions for the authenticated user's organization, ordered by most recent first.

### Parameters

| Name     | Type    | Required | Description                                  |
| -------- | ------- | -------- | -------------------------------------------- |
| `cursor` | string  | No       | Cursor for pagination (query parameter)      |
| `limit`  | integer | No       | Number of results per page (query parameter) |

### Request

<CodeGroup>
  ```bash cURL theme={null}
  curl "https://api.avala.ai/api/v1/webhooks/" \
    -H "X-Avala-Api-Key: $AVALA_API_KEY"
  ```

  ```python Python theme={null}
  import requests

  response = requests.get(
      "https://api.avala.ai/api/v1/webhooks/",
      headers={"X-Avala-Api-Key": "YOUR_API_KEY"}
  )
  webhooks = response.json()["results"]
  ```

  ```typescript TypeScript theme={null}
  const response = await fetch(
    "https://api.avala.ai/api/v1/webhooks/",
    {
      headers: { "X-Avala-Api-Key": "YOUR_API_KEY" },
    }
  );
  const { results } = await response.json();
  ```

  ```go Go theme={null}
  req, _ := http.NewRequest("GET", "https://api.avala.ai/api/v1/webhooks/", nil)
  req.Header.Set("X-Avala-Api-Key", "YOUR_API_KEY")

  resp, err := http.DefaultClient.Do(req)
  ```
</CodeGroup>

### Response

```json theme={null}
{
  "next": null,
  "previous": null,
  "results": [
    {
      "uid": "c2d3e4f5-a6b7-8c9d-0e1f-2a3b4c5d6e7f",
      "target_url": "https://my-app.example.com/avala-webhook",
      "events": ["result.submitted", "export.completed"],
      "is_active": true,
      "created_at": "2025-02-10T08:00:00Z",
      "updated_at": "2025-02-10T08:00:00Z"
    }
  ]
}
```

### Fields

| Field        | Type              | Description                                        |
| ------------ | ----------------- | -------------------------------------------------- |
| `uid`        | string (UUID)     | Unique identifier for the webhook subscription     |
| `target_url` | string            | HTTPS URL where event payloads are delivered       |
| `events`     | array             | List of event types this webhook subscribes to     |
| `is_active`  | boolean           | Whether the webhook is currently enabled           |
| `created_at` | string (datetime) | ISO 8601 timestamp of when the webhook was created |
| `updated_at` | string (datetime) | ISO 8601 timestamp of the last update              |

***

## Create Webhook

```
POST /api/v1/webhooks/
```

Creates a new webhook subscription. If no `secret` is provided, one is generated automatically.

The `secret` field is returned **only** in the creation response. Store it securely -- it is used to verify delivery signatures and cannot be retrieved later.

### Parameters

| Name         | Type    | Required | Description                                                   |
| ------------ | ------- | -------- | ------------------------------------------------------------- |
| `target_url` | string  | Yes      | HTTPS URL to receive webhook payloads                         |
| `events`     | array   | Yes      | Event types to subscribe to (see [Event Types](#event-types)) |
| `secret`     | string  | No       | Signing secret (auto-generated if not provided, write-only)   |
| `is_active`  | boolean | No       | Whether the webhook is enabled (default: `true`)              |

### Request

<CodeGroup>
  ```bash cURL theme={null}
  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://my-app.example.com/avala-webhook",
      "events": ["result.submitted", "export.completed"]
    }'
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      "https://api.avala.ai/api/v1/webhooks/",
      headers={
          "X-Avala-Api-Key": "YOUR_API_KEY",
          "Content-Type": "application/json"
      },
      json={
          "target_url": "https://my-app.example.com/avala-webhook",
          "events": ["result.submitted", "export.completed"]
      }
  )
  webhook = response.json()
  ```

  ```typescript TypeScript theme={null}
  const response = await fetch(
    "https://api.avala.ai/api/v1/webhooks/",
    {
      method: "POST",
      headers: {
        "X-Avala-Api-Key": "YOUR_API_KEY",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        target_url: "https://my-app.example.com/avala-webhook",
        events: ["result.submitted", "export.completed"],
      }),
    }
  );
  const webhook = await response.json();
  ```

  ```go Go theme={null}
  body := strings.NewReader(`{
      "target_url": "https://my-app.example.com/avala-webhook",
      "events": ["result.submitted", "export.completed"]
  }`)

  req, _ := http.NewRequest("POST", "https://api.avala.ai/api/v1/webhooks/", body)
  req.Header.Set("X-Avala-Api-Key", "YOUR_API_KEY")
  req.Header.Set("Content-Type", "application/json")

  resp, err := http.DefaultClient.Do(req)
  ```
</CodeGroup>

### Response

```json theme={null}
{
  "uid": "c2d3e4f5-a6b7-8c9d-0e1f-2a3b4c5d6e7f",
  "target_url": "https://my-app.example.com/avala-webhook",
  "secret": "e8b2c4a6d0f1e3b5a7c9d1f3e5b7a9c1d3f5e7b9a1c3d5f7e9b1a3c5d7f9e1b3",
  "events": ["result.submitted", "export.completed"],
  "is_active": true,
  "created_at": "2025-02-10T08:00:00Z",
  "updated_at": "2025-02-10T08:00:00Z"
}
```

<Warning>
  The `secret` is only returned once, at creation time. Store it securely. Use it to verify the `X-Avala-Signature` header on incoming deliveries.
</Warning>

***

## Get Webhook

```
GET /api/v1/webhooks/{uid}/
```

Returns the details of a specific webhook subscription.

### Parameters

| Name  | Type          | Required | Description                               |
| ----- | ------------- | -------- | ----------------------------------------- |
| `uid` | string (UUID) | Yes      | Webhook subscription UID (path parameter) |

### Request

<CodeGroup>
  ```bash cURL theme={null}
  curl "https://api.avala.ai/api/v1/webhooks/c2d3e4f5-a6b7-8c9d-0e1f-2a3b4c5d6e7f/" \
    -H "X-Avala-Api-Key: $AVALA_API_KEY"
  ```

  ```python Python theme={null}
  import requests

  response = requests.get(
      "https://api.avala.ai/api/v1/webhooks/c2d3e4f5-a6b7-8c9d-0e1f-2a3b4c5d6e7f/",
      headers={"X-Avala-Api-Key": "YOUR_API_KEY"}
  )
  webhook = response.json()
  ```

  ```typescript TypeScript theme={null}
  const response = await fetch(
    "https://api.avala.ai/api/v1/webhooks/c2d3e4f5-a6b7-8c9d-0e1f-2a3b4c5d6e7f/",
    {
      headers: { "X-Avala-Api-Key": "YOUR_API_KEY" },
    }
  );
  const webhook = await response.json();
  ```

  ```go Go theme={null}
  req, _ := http.NewRequest("GET", "https://api.avala.ai/api/v1/webhooks/c2d3e4f5-a6b7-8c9d-0e1f-2a3b4c5d6e7f/", nil)
  req.Header.Set("X-Avala-Api-Key", "YOUR_API_KEY")

  resp, err := http.DefaultClient.Do(req)
  ```
</CodeGroup>

### Response

```json theme={null}
{
  "uid": "c2d3e4f5-a6b7-8c9d-0e1f-2a3b4c5d6e7f",
  "target_url": "https://my-app.example.com/avala-webhook",
  "events": ["result.submitted", "export.completed"],
  "is_active": true,
  "created_at": "2025-02-10T08:00:00Z",
  "updated_at": "2025-02-10T08:00:00Z"
}
```

### Fields

Same as [List Webhooks fields](#fields).

***

## Update Webhook

```
PUT /api/v1/webhooks/{uid}/
```

Updates an existing webhook subscription. You can also use `PATCH` for partial updates. The secret cannot be changed through this endpoint.

### Parameters

| Name         | Type          | Required  | Description                               |
| ------------ | ------------- | --------- | ----------------------------------------- |
| `uid`        | string (UUID) | Yes       | Webhook subscription UID (path parameter) |
| `target_url` | string        | Yes (PUT) | HTTPS URL to receive webhook payloads     |
| `events`     | array         | Yes (PUT) | Event types to subscribe to               |
| `is_active`  | boolean       | No        | Whether the webhook is enabled            |

### Request

<CodeGroup>
  ```bash cURL theme={null}
  curl -X PATCH "https://api.avala.ai/api/v1/webhooks/c2d3e4f5-a6b7-8c9d-0e1f-2a3b4c5d6e7f/" \
    -H "X-Avala-Api-Key: $AVALA_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "events": ["result.submitted", "result.accepted", "result.rejected"]
    }'
  ```

  ```python Python theme={null}
  import requests

  response = requests.patch(
      "https://api.avala.ai/api/v1/webhooks/c2d3e4f5-a6b7-8c9d-0e1f-2a3b4c5d6e7f/",
      headers={
          "X-Avala-Api-Key": "YOUR_API_KEY",
          "Content-Type": "application/json"
      },
      json={
          "events": ["result.submitted", "result.accepted", "result.rejected"]
      }
  )
  webhook = response.json()
  ```

  ```typescript TypeScript theme={null}
  const response = await fetch(
    "https://api.avala.ai/api/v1/webhooks/c2d3e4f5-a6b7-8c9d-0e1f-2a3b4c5d6e7f/",
    {
      method: "PATCH",
      headers: {
        "X-Avala-Api-Key": "YOUR_API_KEY",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        events: ["result.submitted", "result.accepted", "result.rejected"],
      }),
    }
  );
  const webhook = await response.json();
  ```

  ```go Go theme={null}
  body := strings.NewReader(`{
      "events": ["result.submitted", "result.accepted", "result.rejected"]
  }`)

  req, _ := http.NewRequest("PATCH", "https://api.avala.ai/api/v1/webhooks/c2d3e4f5-a6b7-8c9d-0e1f-2a3b4c5d6e7f/", body)
  req.Header.Set("X-Avala-Api-Key", "YOUR_API_KEY")
  req.Header.Set("Content-Type", "application/json")

  resp, err := http.DefaultClient.Do(req)
  ```
</CodeGroup>

### Response

```json theme={null}
{
  "uid": "c2d3e4f5-a6b7-8c9d-0e1f-2a3b4c5d6e7f",
  "target_url": "https://my-app.example.com/avala-webhook",
  "events": ["result.submitted", "result.accepted", "result.rejected"],
  "is_active": true,
  "created_at": "2025-02-10T08:00:00Z",
  "updated_at": "2025-02-10T12:00:00Z"
}
```

***

## Delete Webhook

```
DELETE /api/v1/webhooks/{uid}/
```

Permanently deletes a webhook subscription. Pending deliveries for this webhook will not be retried.

### Parameters

| Name  | Type          | Required | Description                               |
| ----- | ------------- | -------- | ----------------------------------------- |
| `uid` | string (UUID) | Yes      | Webhook subscription UID (path parameter) |

### Request

<CodeGroup>
  ```bash cURL theme={null}
  curl -X DELETE "https://api.avala.ai/api/v1/webhooks/c2d3e4f5-a6b7-8c9d-0e1f-2a3b4c5d6e7f/" \
    -H "X-Avala-Api-Key: $AVALA_API_KEY"
  ```

  ```python Python theme={null}
  import requests

  response = requests.delete(
      "https://api.avala.ai/api/v1/webhooks/c2d3e4f5-a6b7-8c9d-0e1f-2a3b4c5d6e7f/",
      headers={"X-Avala-Api-Key": "YOUR_API_KEY"}
  )
  ```

  ```typescript TypeScript theme={null}
  const response = await fetch(
    "https://api.avala.ai/api/v1/webhooks/c2d3e4f5-a6b7-8c9d-0e1f-2a3b4c5d6e7f/",
    {
      method: "DELETE",
      headers: { "X-Avala-Api-Key": "YOUR_API_KEY" },
    }
  );
  ```

  ```go Go theme={null}
  req, _ := http.NewRequest("DELETE", "https://api.avala.ai/api/v1/webhooks/c2d3e4f5-a6b7-8c9d-0e1f-2a3b4c5d6e7f/", nil)
  req.Header.Set("X-Avala-Api-Key", "YOUR_API_KEY")

  resp, err := http.DefaultClient.Do(req)
  ```
</CodeGroup>

### Response

Returns `204 No Content` on success.

***

## Test Webhook

```
POST /api/v1/webhooks/{uid}/test/
```

Sends a test `ping` event to the webhook's target URL. The delivery is processed asynchronously -- check the [List Deliveries](#list-deliveries) endpoint to see the result.

### Parameters

| Name  | Type          | Required | Description                               |
| ----- | ------------- | -------- | ----------------------------------------- |
| `uid` | string (UUID) | Yes      | Webhook subscription UID (path parameter) |

### Request

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.avala.ai/api/v1/webhooks/c2d3e4f5-a6b7-8c9d-0e1f-2a3b4c5d6e7f/test/" \
    -H "X-Avala-Api-Key: $AVALA_API_KEY"
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      "https://api.avala.ai/api/v1/webhooks/c2d3e4f5-a6b7-8c9d-0e1f-2a3b4c5d6e7f/test/",
      headers={"X-Avala-Api-Key": "YOUR_API_KEY"}
  )
  result = response.json()
  ```

  ```typescript TypeScript theme={null}
  const response = await fetch(
    "https://api.avala.ai/api/v1/webhooks/c2d3e4f5-a6b7-8c9d-0e1f-2a3b4c5d6e7f/test/",
    {
      method: "POST",
      headers: { "X-Avala-Api-Key": "YOUR_API_KEY" },
    }
  );
  const result = await response.json();
  ```

  ```go Go theme={null}
  req, _ := http.NewRequest("POST", "https://api.avala.ai/api/v1/webhooks/c2d3e4f5-a6b7-8c9d-0e1f-2a3b4c5d6e7f/test/", nil)
  req.Header.Set("X-Avala-Api-Key", "YOUR_API_KEY")

  resp, err := http.DefaultClient.Do(req)
  ```
</CodeGroup>

### Response

```json theme={null}
{
  "detail": "Test webhook queued.",
  "delivery_uid": "f6a7b8c9-d0e1-2f3a-4b5c-6d7e8f9a0b1c"
}
```

***

## List Deliveries

```
GET /api/v1/webhook-deliveries/
```

Returns all webhook delivery attempts for the authenticated user's organization, ordered by most recent first.

### Parameters

| Name     | Type    | Required | Description                                  |
| -------- | ------- | -------- | -------------------------------------------- |
| `cursor` | string  | No       | Cursor for pagination (query parameter)      |
| `limit`  | integer | No       | Number of results per page (query parameter) |

### Request

<CodeGroup>
  ```bash cURL theme={null}
  curl "https://api.avala.ai/api/v1/webhook-deliveries/" \
    -H "X-Avala-Api-Key: $AVALA_API_KEY"
  ```

  ```python Python theme={null}
  import requests

  response = requests.get(
      "https://api.avala.ai/api/v1/webhook-deliveries/",
      headers={"X-Avala-Api-Key": "YOUR_API_KEY"}
  )
  deliveries = response.json()["results"]
  ```

  ```typescript TypeScript theme={null}
  const response = await fetch(
    "https://api.avala.ai/api/v1/webhook-deliveries/",
    {
      headers: { "X-Avala-Api-Key": "YOUR_API_KEY" },
    }
  );
  const { results } = await response.json();
  ```

  ```go Go theme={null}
  req, _ := http.NewRequest("GET", "https://api.avala.ai/api/v1/webhook-deliveries/", nil)
  req.Header.Set("X-Avala-Api-Key", "YOUR_API_KEY")

  resp, err := http.DefaultClient.Do(req)
  ```
</CodeGroup>

### Response

```json theme={null}
{
  "next": null,
  "previous": null,
  "results": [
    {
      "uid": "f6a7b8c9-d0e1-2f3a-4b5c-6d7e8f9a0b1c",
      "subscription": "c2d3e4f5-a6b7-8c9d-0e1f-2a3b4c5d6e7f",
      "event_type": "result.submitted",
      "payload": {
        "event": "result.submitted",
        "result_uid": "990c1800-b62f-85a8-e150-880099881111"
      },
      "response_status": 200,
      "response_body": "{\"ok\": true}",
      "attempts": 1,
      "next_retry_at": null,
      "status": "delivered",
      "created_at": "2025-02-10T14:00:00Z",
      "updated_at": "2025-02-10T14:00:01Z"
    }
  ]
}
```

### Delivery Fields

| Field             | Type                      | Description                                                   |
| ----------------- | ------------------------- | ------------------------------------------------------------- |
| `uid`             | string (UUID)             | Unique identifier for the delivery                            |
| `subscription`    | string (UUID)             | UID of the webhook subscription                               |
| `event_type`      | string                    | Event type that triggered this delivery                       |
| `payload`         | object                    | Event payload that was sent                                   |
| `response_status` | integer \| null           | HTTP status code from the target URL                          |
| `response_body`   | string \| null            | Response body from the target URL                             |
| `attempts`        | integer                   | Number of delivery attempts made                              |
| `next_retry_at`   | string (datetime) \| null | When the next retry is scheduled, if applicable               |
| `status`          | string                    | Delivery status (see [Delivery Statuses](#delivery-statuses)) |
| `created_at`      | string (datetime)         | ISO 8601 timestamp of creation                                |
| `updated_at`      | string (datetime)         | ISO 8601 timestamp of the last update                         |

***

## Get Delivery

```
GET /api/v1/webhook-deliveries/{uid}/
```

Returns the details of a specific webhook delivery.

### Parameters

| Name  | Type          | Required | Description                   |
| ----- | ------------- | -------- | ----------------------------- |
| `uid` | string (UUID) | Yes      | Delivery UID (path parameter) |

### Request

<CodeGroup>
  ```bash cURL theme={null}
  curl "https://api.avala.ai/api/v1/webhook-deliveries/f6a7b8c9-d0e1-2f3a-4b5c-6d7e8f9a0b1c/" \
    -H "X-Avala-Api-Key: $AVALA_API_KEY"
  ```

  ```python Python theme={null}
  import requests

  response = requests.get(
      "https://api.avala.ai/api/v1/webhook-deliveries/f6a7b8c9-d0e1-2f3a-4b5c-6d7e8f9a0b1c/",
      headers={"X-Avala-Api-Key": "YOUR_API_KEY"}
  )
  delivery = response.json()
  ```

  ```typescript TypeScript theme={null}
  const response = await fetch(
    "https://api.avala.ai/api/v1/webhook-deliveries/f6a7b8c9-d0e1-2f3a-4b5c-6d7e8f9a0b1c/",
    {
      headers: { "X-Avala-Api-Key": "YOUR_API_KEY" },
    }
  );
  const delivery = await response.json();
  ```

  ```go Go theme={null}
  req, _ := http.NewRequest("GET", "https://api.avala.ai/api/v1/webhook-deliveries/f6a7b8c9-d0e1-2f3a-4b5c-6d7e8f9a0b1c/", nil)
  req.Header.Set("X-Avala-Api-Key", "YOUR_API_KEY")

  resp, err := http.DefaultClient.Do(req)
  ```
</CodeGroup>

### Response

Same format as individual items in [List Deliveries](#list-deliveries).

***

## Event Types

| Event                  | Description                             |
| ---------------------- | --------------------------------------- |
| `dataset.created`      | A new dataset was created               |
| `dataset.updated`      | An existing dataset was updated         |
| `dataset.deleted`      | A dataset was deleted                   |
| `export.completed`     | An export job completed successfully    |
| `export.failed`        | An export job failed                    |
| `task.completed`       | A task was marked as complete           |
| `result.submitted`     | An annotation result was submitted      |
| `result.accepted`      | An annotation result was accepted       |
| `result.rejected`      | An annotation result was rejected       |
| `auto_label.completed` | An auto-label job finished successfully |
| `auto_label.failed`    | An auto-label job failed                |
| `quality.alert`        | A quality target threshold was breached |

***

## Delivery Statuses

| Status      | Description                                                |
| ----------- | ---------------------------------------------------------- |
| `pending`   | Delivery is queued and has not been attempted yet          |
| `delivered` | Payload was successfully delivered (received 2xx response) |
| `failed`    | All delivery attempts failed                               |

## Retry Behavior

Failed deliveries are retried automatically with exponential backoff. Avala makes up to **5 retry attempts** before marking a delivery as `failed`.

| Attempt     | Delay       | Total Elapsed |
| ----------- | ----------- | ------------- |
| 1 (initial) | Immediate   | 0s            |
| 2           | 60 seconds  | 1 min         |
| 3           | 120 seconds | 3 min         |
| 4           | 240 seconds | 7 min         |
| 5           | 480 seconds | 15 min        |
| 6           | 960 seconds | 31 min        |

**Retry rules:**

* **2xx responses** are treated as successful. No retry.
* **4xx responses** (client errors) are **not retried**. These indicate a problem with your endpoint (bad URL, auth failure, rejected payload). Fix your endpoint and use the [Test Webhook](#test-webhook) endpoint to re-verify.
* **5xx responses** and **network errors** (timeout, connection refused) are retried with the backoff schedule above.
* The `next_retry_at` field on a delivery shows when the next attempt is scheduled. It is `null` for delivered or permanently failed deliveries.

<Tip>
  Design your webhook endpoint to be **idempotent**. If you receive the same event twice (due to retries or network issues), your handler should produce the same result. Use the delivery `uid` as a deduplication key.
</Tip>

### Signature Verification

Every delivery includes an `X-Avala-Webhook-Signature` header. Verify it using the `secret` from when you created the webhook:

```python theme={null}
import hashlib
import hmac

def verify_signature(payload_body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.HMAC(
        secret.encode(), payload_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)
```

Always verify signatures before processing webhook payloads to prevent spoofed events.

***

## Error Responses

### Validation Error (400)

```json theme={null}
{
  "events": ["Invalid events: invalid.event"],
  "target_url": ["A webhook subscription for this URL already exists."]
}
```

Returned when the request body contains invalid events or a duplicate target URL.

### Unauthorized (401)

```json theme={null}
{
  "detail": "Invalid API key."
}
```

Returned when the `X-Avala-Api-Key` header is missing or contains an invalid key.

### Permission Denied (403)

```json theme={null}
{
  "detail": "You do not have permission to perform this action."
}
```

Returned when the authenticated user does not have access to the requested webhook.

### Not Found (404)

```json theme={null}
{
  "detail": "Not found."
}
```

Returned when the specified webhook or delivery UID does not exist.
