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

# Webhooks Overview

> Receive real-time updates from Altur directly to your backend or CRM.

## Introduction to Webhooks

Webhooks allow you to receive real-time notifications about events in your Altur organization. When a specific event occurs (e.g., the end of a call), Altur sends an HTTP POST request with event-specific data to your configured endpoint. This enables seamless integration with your backend systems or CRM, ensuring they remain up-to-date.

## How Webhooks Work

1. **Configure Your Endpoint**\
   You can configure your webhook URL in the Altur platform. This URL will receive POST requests when specific events are triggered.

2. **Receive Notifications**\
   When an event occurs, Altur sends a POST request with the event's details in a structured JSON format to your endpoint.

3. **Acknowledge Delivery**\
   Your endpoint should respond with a `200 OK` status code to confirm successful receipt. Altur waits up to 5 seconds for a response. Timeouts, connection errors, `5xx`, `408` and `429` responses are retried with exponential backoff (up to 5 total attempts). Any other `4xx` response fails immediately without retries, and redirects are not followed. See the [Delivery Retry Policy](#delivery-retry-policy).

## Event Types

Webhooks can be triggered by the following event types:

* [`on_call_end`](https://docs.altur.io/api-reference/webhooks/on-call-end): Triggered at the end of a call, this webhook sends detailed information about the call along with basic information about the AI agent and the end user.
* [`transfer_started`](https://docs.altur.io/api-reference/webhooks/transfer-started): Triggered when a call transfer starts, so your human agents can prepare with real-time call context.
* [`transfer_completed`](https://docs.altur.io/api-reference/webhooks/transfer-completed): Triggered when a call transfer connects to the human leg.
* [`campaign.status_changed`](https://docs.altur.io/api-reference/webhooks/campaign-status-changed): Triggered when a Campaign transitions between lifecycle statuses. Includes the analytics snapshot when the campaign reaches `finished`.
* [`campaign.cycle_completed`](https://docs.altur.io/api-reference/webhooks/campaign-cycle-completed): Triggered when a cycle iteration of a Campaign finishes, with the completed iteration number and the next scheduled iteration timestamp.

<Note>
  Campaign webhooks use a versioned envelope (`event_id`, `event_type`,
  `occurred_at`, `api_version`, `project_id`, `data`). Use `event_id` for
  idempotent processing on your side. The call events (`on_call_end`,
  `transfer_started`, `transfer_completed`) predate this envelope, keep their
  flat top-level shape and have no `event_id`: deduplicate them on `id` (the
  call ID) plus `event_type`.
</Note>

## Securing Your Webhooks

To ensure secure communication and verify the authenticity of webhook requests, Altur includes an HMAC signature in the `X-Altur-Signature` header for every request.

### How It Works

* **Shared Secret**: Each webhook integration is configured with a unique base64-encoded shared secret key.
* **HMAC Generation**: Altur generates an HMAC SHA-256 hash using the base64-decoded shared secret and the payload serialized as compact JSON (no spaces after `:` or `,`, non-ASCII characters escaped as `\uXXXX`, e.g. `Sofía` → `Sof\u00eda`). This hash is base64-encoded and included in the `X-Altur-Signature` header.
* **Validation**: Your endpoint must parse the body and re-serialize it in that same compact form before computing the HMAC.

<Warning>
  The request body Altur sends is **not** the exact string it signs: the body
  includes spaces after `:` and `,`. Computing the HMAC over the raw request
  body will fail. Always parse the JSON and re-serialize it as described above.
</Warning>

### Validation Function Example

<CodeGroup>
  ```python Python theme={null}
  import base64
  import hashlib
  import hmac
  import json


  def is_valid_signature(secret: str, payload: dict, signature: str) -> bool:
      """
      Validate Altur webhook signature.

      Args:
          secret: Base64-encoded shared secret key
          payload: Parsed request body (not the raw body)
          signature: Base64-encoded signature from X-Altur-Signature header
      """
      # Re-serialize in compact form. json.dumps escapes non-ASCII
      # characters as \uXXXX by default, which matches Altur.
      payload_bytes = json.dumps(payload, separators=(",", ":")).encode("utf-8")

      # Decode the base64 secret
      secret_bytes = base64.b64decode(secret)

      # Generate expected signature
      expected_signature = base64.b64encode(
          hmac.new(secret_bytes, payload_bytes, hashlib.sha256).digest()
      ).decode()

      return hmac.compare_digest(signature, expected_signature)
  ```

  ```javascript JavaScript theme={null}
  const crypto = require("crypto");

  function isValidSignature(secret, payload, signature) {
    /**
     * Validate Altur webhook signature.
     *
     * @param {string} secret - Base64-encoded shared secret key
     * @param {object} payload - Parsed request body (not the raw body)
     * @param {string} signature - Base64-encoded signature from X-Altur-Signature header
     */

    // Re-serialize in compact form and escape non-ASCII characters as \uXXXX
    // (JSON.stringify leaves them as-is, Altur escapes them)
    const payloadString = JSON.stringify(payload).replace(
      /[\u0080-\uffff]/g,
      (c) => "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0")
    );

    // Decode base64 secret
    const secretBuffer = Buffer.from(secret, "base64");

    // Generate expected signature
    const expected = crypto
      .createHmac("sha256", secretBuffer)
      .update(payloadString)
      .digest();
    const received = Buffer.from(signature || "", "base64");

    // timingSafeEqual throws when lengths differ, so compare lengths first
    return (
      received.length === expected.length &&
      crypto.timingSafeEqual(received, expected)
    );
  }

  // Usage example (with express.json())
  const signature = req.headers["x-altur-signature"];
  const isValid = isValidSignature(yourSecret, req.body, signature);
  ```

  ```php PHP theme={null}
  function isValidSignature($secret, $payload, $signature) {
      /*
       * Validate Altur webhook signature.
       *
       * @param string $secret Base64-encoded shared secret key
       * @param object $payload Body decoded with json_decode($raw), WITHOUT `true`
       * @param string $signature Base64-encoded signature from X-Altur-Signature header
       */

      // Re-serialize in compact form. json_encode escapes non-ASCII
      // characters as \uXXXX by default, which matches Altur.
      $payloadString = json_encode($payload, JSON_UNESCAPED_SLASHES);

      // Decode base64 secret
      $secretBytes = base64_decode($secret);

      // Generate expected signature
      $expectedSignature = base64_encode(
          hash_hmac('sha256', $payloadString, $secretBytes, true)
      );

      // Compare signatures using timing-safe comparison
      return hash_equals($expectedSignature, (string) $signature);
  }

  // Usage example
  $signature = $_SERVER['HTTP_X_ALTUR_SIGNATURE'] ?? '';
  // Decode as objects: json_decode(..., true) turns `{}` (e.g. an empty
  // extracted_data) into `[]`, which re-encodes differently and breaks the signature.
  $payload = json_decode(file_get_contents('php://input'));
  $isValid = isValidSignature($yourSecret, $payload, $signature);
  ```
</CodeGroup>

### Usage Example

```python theme={null}
request_body = request.get_json()  # Parsed JSON from your web framework
signature = request.headers.get("X-Altur-Signature", "")
shared_secret = "your-base64-encoded-shared-secret"

if is_valid_signature(shared_secret, request_body, signature):
    print("Valid webhook request!")
else:
    print("Invalid webhook request!")
```

### Headers Sent with Webhook

* `Content-Type`: `application/json`
* `X-Altur-Signature`: Base64-encoded HMAC SHA-256 hash of the compact JSON payload (see above; this is not the raw body)

### Why This Matters

This mechanism ensures:

* The request originates from Altur.
* The payload has not been tampered with during transmission.

## Delivery Retry Policy

Any response other than `2xx` counts as a failure. Whether Altur retries depends on the failure:

| Failure | Behavior |
| :- | :- |
| Timeout (no response within 5 seconds) | Retried |
| Connection error (DNS, TLS, refused connection, etc.) | Retried |
| `5xx` | Retried |
| `408 Request Timeout`, `429 Too Many Requests` | Retried |
| Any other `4xx` (e.g. `400`, `401`, `403`, `404`) | **Terminal**: fails immediately, no retries |
| `3xx` redirect | **Terminal**: redirects are not followed and count as a failure |

Retries use:

* **Maximum attempts**: 5 (initial delivery plus 4 retries).
* **Backoff**: 60s after the first failure, doubling each attempt (60s, 2m, 4m, 8m).

If all attempts fail, or the failure is terminal, the event is marked as failed and will not be delivered again.

<Tip>
  If your endpoint can't process an event temporarily, respond with `5xx` or
  `429` so Altur retries. A `4xx` such as `400` tells Altur the event will never
  be accepted. Configure the final URL directly: a redirect (for example
  `http` → `https`, or a missing trailing slash) makes every delivery fail.
</Tip>

## Example Workflow

Here’s a typical workflow for handling webhooks effectively:

1. **Set Up Your Endpoint**

   Create an API endpoint in your backend, such as `/webhooks/altur/on-call-end`. Ensure the endpoint is publicly accessible and configured to accept POST requests with a `Content-Type` of `application/json`.

2. **Parse Incoming Data**

   Extract and process the JSON payload sent by Altur. Ensure your code handles possible issues, such as malformed payloads or missing fields, to prevent runtime errors.

3. **Validate Request Authenticity**

   Use the `X-Altur-Signature` header to verify the authenticity of the request. This involves:

   * Recomputing the HMAC signature using the shared secret and the parsed payload re-serialized as compact JSON.
   * Comparing it with the signature provided in the header.

4. **Respond Promptly**

   Return a `200 OK` status code to acknowledge successful receipt. Altur times out the request after 5 seconds, so respond well before that to avoid unnecessary retries.

5. **Log Events for Debugging**

   Log the incoming request and processing steps to ensure traceability. This is particularly useful for debugging issues with payloads or retries.

6. **Handle Retries Gracefully**

   Design your system to handle retries in case of temporary failures. Ensure your endpoint is idempotent so processing the same event multiple times does not create duplicate actions (e.g., duplicate database entries or API calls).

## Best Practices

1. **Secure Your Endpoint**

   * **Authenticate Requests**: Verify webhook authenticity using HMAC signatures. Always use a secure and unique shared secret for each integration.
   * **Restrict Access**: Use IP whitelisting or firewalls to allow requests only from Altur's servers.
   * **Encrypt Communication**: Ensure your webhook endpoint uses HTTPS to encrypt data in transit.

2. **Log Events**

   * **Record Everything**: Log all incoming webhook requests, including timestamps, headers, payloads, and responses.
   * **Monitor Failures**: Implement alerts for repeated failures or invalid requests to quickly address issues.
   * **Enable Debugging**: Retain logs for a reasonable period to aid in debugging or auditing as necessary.

3. **Test Thoroughly**

   * **Simulate Events**: Use the Altur dashboard to simulate webhook events and confirm your endpoint handles them correctly.
   * **Handle Edge Cases**: Test scenarios like malformed payloads, large payloads, or missing fields to ensure robustness.
   * **Use Staging Environments**: Set up a dedicated staging endpoint for safe testing without affecting production systems.

4. **Optimize Performance**

   * **Respond Quickly**: Altur times out after 5 seconds. If processing takes longer, acknowledge with `200 OK` immediately and handle the work asynchronously.
   * **Minimize Processing**: Perform lightweight validation and queuing at the webhook level, offloading heavy processing to background workers.
   * **Use Caching**: Cache static responses for redundant requests when appropriate.

5. **Ensure Idempotency**

   * **Avoid Duplicate Actions**: Design your system to handle retries without causing duplicate operations (e.g., database inserts or API calls). For campaign events, deduplicate on `event_id`. Call events (`on_call_end`, `transfer_started`, `transfer_completed`) have no `event_id`, so deduplicate on `id` plus `event_type`.

6. **Communicate Failures**
   * **Return Meaningful Status Codes**: Respond with appropriate HTTP status codes to help diagnose issues (e.g., `400` for invalid requests or `500` for server errors). Remember that `4xx` responses other than `408` and `429` are not retried.
   * **Log and Notify**: Log failures and consider notifying your team if critical retries are exhausted.

## Troubleshooting Signature Validation

If you're having trouble with signature validation, check these common issues:

### 1. JSON Serialization Format

Ensure you're using the same JSON serialization format as Altur:

* **Parse, then re-serialize**: Don't compute the HMAC over the raw body. It contains spaces and won't match.
* **Compact format**: No spaces after colons or commas.
* **Escaped non-ASCII**: Characters like `í` or `ñ` must be escaped as `\uXXXX` (`Sofía` → `Sof\u00eda`). `JSON.stringify` doesn't do this; see the JavaScript example.
* **Consistent ordering**: Keep keys in the order they were received.
* **PHP**: Use `json_decode($raw)`, not `json_decode($raw, true)`. Associative arrays turn `{}` into `[]`.

```python theme={null}
# ✅ Correct - compact JSON
json.dumps(data, separators=(',', ':'))

# ❌ Wrong - includes spaces
json.dumps(data)  # Results in: {"key": "value"}

# ❌ Wrong - non-ASCII not escaped
json.dumps(data, separators=(',', ':'), ensure_ascii=False)
```

### 2. Secret Key Format

* Ensure your secret key is **base64-encoded**
* The secret should be the same one configured in your Altur webhook integration

### 3. Header Name

* The signature header is `X-Altur-Signature` (case-sensitive in some frameworks)
* Make sure you're reading the correct header

### 4. Timing Attacks

Always use timing-safe comparison functions:

* Python: `hmac.compare_digest()`
* Node.js: `crypto.timingSafeEqual()`
* PHP: `hash_equals()`

### 5. Testing Your Implementation

You can test your signature validation with this example:

```python theme={null}
# Test data
test_payload = {"test": "data"}
test_secret = "dGVzdC1zZWNyZXQ="  # base64 for "test-secret"
expected_signature = "6RQRnEGhLZoG7UMG+fPBzjLuZhjLsG9U8AooC6LceYU="

# This should return True
result = is_valid_signature(test_secret, test_payload, expected_signature)
print(f"Validation result: {result}")
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.