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

# Webhooks — Receive Real-Time Events from Novala

> Register an HTTPS endpoint to receive Novala events, verify request authenticity with HMAC-SHA256 signatures, and understand retry and auto-disable behavior.

Webhooks let Novala push real-time event notifications to your server whenever something happens in the platform — a lead is created, a deal changes stage, a booking is confirmed, and more. You register an endpoint URL in the dashboard, and Novala sends a POST request to that URL with a JSON payload each time a matching event occurs.

## Registering a webhook endpoint

<Steps>
  <Step title="Open webhook settings">
    In the Novala dashboard, navigate to **Settings → Webhooks**.
  </Step>

  <Step title="Add your endpoint">
    Click **Add Endpoint**, enter your publicly accessible HTTPS URL, and select the event types you want to receive. To receive all events, select **All events** or subscribe to `*`.
  </Step>

  <Step title="Save the signing secret">
    After saving, copy the signing secret shown in the dialog. You will use it to verify incoming requests. It is not shown again after you close the dialog.
  </Step>
</Steps>

<Warning>
  Webhook endpoints must be reachable over HTTPS. HTTP endpoints are not supported.
</Warning>

## Payload format

Novala sends a `POST` request with `Content-Type: application/json` to your endpoint. The request body has this shape:

```json theme={null}
{
  "id": "evt_01j1abc123xyz",
  "type": "bookings.confirmed",
  "tenantId": "tenant-uuid-here",
  "timestamp": "2024-08-01T09:05:22.341Z",
  "source": "bookings",
  "data": {
    "bookingId": "booking-uuid-001",
    "resourceId": "res-uuid-001",
    "resourceName": "Main Campus Tour",
    "startsAt": "2024-08-01T10:00:00Z",
    "endsAt": "2024-08-01T11:00:00Z",
    "email": "morgan.lee@example.com",
    "firstName": "Morgan",
    "lastName": "Lee"
  }
}
```

| Field       | Type   | Description                                                                   |
| ----------- | ------ | ----------------------------------------------------------------------------- |
| `id`        | string | Unique delivery ID for this event.                                            |
| `type`      | string | Event type name (for example, `pipeline.deal.created`).                       |
| `tenantId`  | string | UUID of the tenant that generated the event.                                  |
| `timestamp` | string | ISO 8601 timestamp of when the event occurred.                                |
| `source`    | string | Module that emitted the event (for example, `bookings`, `pipeline`, `leads`). |
| `data`      | object | Event-specific payload. Shape varies by event type.                           |

## Request headers

Novala includes these headers on every webhook delivery:

| Header                 | Description                                                  |
| ---------------------- | ------------------------------------------------------------ |
| `X-Novala-Signature`   | HMAC-SHA256 signature for verifying the payload (see below). |
| `X-Novala-Timestamp`   | ISO 8601 timestamp used in the signature computation.        |
| `X-Novala-Delivery-Id` | UUID identifying this specific delivery attempt.             |
| `X-Novala-Event-Type`  | Event type string (same as `type` in the body).              |
| `User-Agent`           | `Novala-Webhooks/1.0`                                        |

## Verifying signatures

Always verify the `X-Novala-Signature` header before processing a webhook. Verification confirms the request genuinely came from Novala and that the body was not tampered with in transit.

The signature is computed as:

```
HMAC-SHA256(secret, "{timestamp}.{rawBody}")
```

The result is hex-encoded and prefixed with `sha256=`.

### Verification example

<CodeGroup>
  ```typescript Node.js / TypeScript theme={null}
  import { createHmac, timingSafeEqual } from 'crypto';

  function verifyNovalaWebhook(
    rawBody: Buffer,
    signature: string,
    timestamp: string,
    secret: string
  ): boolean {
    const message = `${timestamp}.${rawBody.toString('utf8')}`;
    const expected = 'sha256=' + createHmac('sha256', secret)
      .update(message)
      .digest('hex');

    // Use timing-safe comparison to prevent timing attacks
    try {
      return timingSafeEqual(
        Buffer.from(signature, 'utf8'),
        Buffer.from(expected, 'utf8')
      );
    } catch {
      return false;
    }
  }

  // Express example
  app.post('/webhooks/novala', express.raw({ type: 'application/json' }), (req, res) => {
    const signature = req.headers['x-novala-signature'] as string;
    const timestamp = req.headers['x-novala-timestamp'] as string;

    if (!verifyNovalaWebhook(req.body, signature, timestamp, process.env.NOVALA_WEBHOOK_SECRET!)) {
      return res.status(401).json({ error: 'Invalid signature' });
    }

    const event = JSON.parse(req.body.toString());
    
    switch (event.type) {
      case 'bookings.confirmed':
        // Handle booking confirmation
        break;
      case 'pipeline.deal.created':
        // Handle new deal
        break;
      case 'leads.lead.created':
        // Handle new lead
        break;
    }

    res.status(200).json({ received: true });
  });
  ```

  ```python Python theme={null}
  import hmac
  import hashlib
  import os
  from flask import Flask, request, abort

  app = Flask(__name__)
  WEBHOOK_SECRET = os.environ['NOVALA_WEBHOOK_SECRET']

  def verify_signature(raw_body: bytes, signature: str, timestamp: str) -> bool:
      message = f"{timestamp}.{raw_body.decode('utf-8')}"
      expected = 'sha256=' + hmac.new(
          WEBHOOK_SECRET.encode('utf-8'),
          message.encode('utf-8'),
          hashlib.sha256
      ).hexdigest()
      return hmac.compare_digest(signature, expected)

  @app.route('/webhooks/novala', methods=['POST'])
  def handle_webhook():
      signature = request.headers.get('X-Novala-Signature', '')
      timestamp = request.headers.get('X-Novala-Timestamp', '')
      
      if not verify_signature(request.get_data(), signature, timestamp):
          abort(401)
      
      event = request.get_json()
      print(f"Received event: {event['type']}")
      return {'received': True}, 200
  ```
</CodeGroup>

<Warning>
  Always use a timing-safe comparison function (such as `timingSafeEqual` in Node.js or `hmac.compare_digest` in Python) when comparing signatures. Regular string equality is vulnerable to timing attacks.
</Warning>

## Responding to webhooks

Your endpoint must return a `2xx` HTTP status code within **10 seconds** to acknowledge receipt. Any response outside the `200–299` range is treated as a delivery failure and triggers a retry.

Return `200` immediately and process the event asynchronously:

```typescript TypeScript theme={null}
app.post('/webhooks/novala', express.raw({ type: 'application/json' }), async (req, res) => {
  // Verify and acknowledge immediately
  if (!verifyNovalaWebhook(req.body, ...)) {
    return res.status(401).end();
  }
  res.status(200).end(); // Acknowledge before processing

  // Process asynchronously
  const event = JSON.parse(req.body.toString());
  await queue.push(event);
});
```

## Retry behavior

If your endpoint returns a non-`2xx` status or does not respond within 10 seconds, Novala retries the delivery with exponential backoff:

| Attempt | Delay after failure |
| ------- | ------------------- |
| 1       | 1 minute            |
| 2       | 5 minutes           |
| 3       | 25 minutes          |
| 4       | 2 hours             |
| 5       | 10 hours            |

After 5 failed attempts the delivery is marked as permanently failed. If an endpoint accumulates **10 consecutive failures** it is automatically disabled. You can re-enable it in **Settings → Webhooks**.

## Common event types

| Event type                    | Triggered when                         |
| ----------------------------- | -------------------------------------- |
| `leads.lead.created`          | A new lead is created.                 |
| `pipeline.deal.created`       | A new deal is created.                 |
| `pipeline.deal.stage-changed` | A deal moves to a different stage.     |
| `pipeline.deal.closed-won`    | A deal is moved to a closed-won stage. |
| `bookings.confirmed`          | A booking is confirmed.                |
| `bookings.cancelled`          | A booking is cancelled.                |
| `bookings.resource.created`   | A new bookable resource is created.    |
| `calso.inspection.submitted`  | An inspection is submitted for review. |
| `contacts.contact.created`    | A new contact is created.              |
