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

# Webhooks

> Receive real-time notifications when events happen in your workspace

# Webhooks

Webhooks let you receive HTTP POST notifications when events occur in your Heffl workspace. Instead of polling the API, your server gets notified in real time when leads are created, deals change stage, invoices are paid, and more.

## Setup

1. Go to **Settings > Developer** in your Heffl workspace
2. Click **Add Webhook Endpoint**
3. Enter your endpoint URL (must be HTTPS in production)
4. Select the events you want to subscribe to
5. Save — you'll receive a signing secret starting with `whsec_`

<Warning>Store your webhook signing secret securely. You'll need it to verify webhook signatures.</Warning>

## API endpoints

* `POST /webhooks` creates a webhook subscription and returns the one-time signing `secret`
* `GET /webhooks` lists subscriptions with cursor pagination
* `GET /webhooks/{id}` returns a single subscription
* `DELETE /webhooks/{id}` deletes a subscription

## Payload format

Every webhook delivery sends a JSON POST request with this structure:

```json theme={null}
{
  "event": "lead.created",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "payload": {
    "id": "ld_abc123",
    "name": "Jane Smith",
    "email": "jane@example.com",
    "mobile": "+1234567890",
    "title": "Product Demo Request",
    "value": 5000,
    "source": "Website",
    "stage": "New",
    "stageId": 1,
    "archived": false,
    "createdAt": "2025-01-15T10:30:00.000Z",
    "customFields": {},
    "ownerId": 42,
    "ownerName": "John Doe"
  }
}
```

## Request headers

Every webhook request includes these headers:

| Header              | Description                                                 |
| ------------------- | ----------------------------------------------------------- |
| `Content-Type`      | `application/json`                                          |
| `User-Agent`        | `Heffl-Webhooks/1.0`                                        |
| `webhook-id`        | Unique message ID (e.g., `msg_2KWPBgLlAfxdpx2AI54pPJ85f4W`) |
| `webhook-timestamp` | Unix timestamp in seconds                                   |
| `webhook-signature` | HMAC-SHA256 signature (see below)                           |

## Verifying signatures

Every webhook is signed using HMAC-SHA256 following the [Standard Webhooks](https://www.standardwebhooks.com) specification. Always verify signatures to ensure the request came from Heffl.

### How it works

1. The signed content is: `{webhook-id}.{webhook-timestamp}.{request-body}`
2. The signature is computed using HMAC-SHA256 with your signing secret
3. The signature header format is: `v1,{base64-encoded-signature}`

### Node.js verification example

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

function verifyWebhook(req, signingSecret) {
  const msgId = req.headers['webhook-id'];
  const timestamp = req.headers['webhook-timestamp'];
  const signature = req.headers['webhook-signature'];
  const body = JSON.stringify(req.body);

  // Check timestamp tolerance (5 minutes)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp)) > 300) {
    throw new Error('Timestamp too old');
  }

  // Compute expected signature
  const secret = Buffer.from(signingSecret.replace('whsec_', ''), 'base64');
  const signedContent = `${msgId}.${timestamp}.${body}`;
  const expectedSig = crypto
    .createHmac('sha256', secret)
    .update(signedContent)
    .digest('base64');

  // Compare signatures
  const receivedSigs = signature.split(' ');
  for (const sig of receivedSigs) {
    const [version, hash] = sig.split(',');
    if (version !== 'v1') continue;

    const expected = Buffer.from(expectedSig);
    const received = Buffer.from(hash);
    if (expected.length === received.length &&
        crypto.timingSafeEqual(expected, received)) {
      return true; // Valid
    }
  }

  throw new Error('Invalid signature');
}
```

### Python verification example

```python theme={null}
import hmac
import hashlib
import base64
import time
import json

def verify_webhook(headers, body, signing_secret):
    msg_id = headers['webhook-id']
    timestamp = headers['webhook-timestamp']
    signature = headers['webhook-signature']

    # Check timestamp tolerance (5 minutes)
    now = int(time.time())
    if abs(now - int(timestamp)) > 300:
        raise ValueError('Timestamp too old')

    # Compute expected signature
    secret = base64.b64decode(signing_secret.replace('whsec_', ''))
    signed_content = f"{msg_id}.{timestamp}.{body}".encode()
    expected_sig = base64.b64encode(
        hmac.new(secret, signed_content, hashlib.sha256).digest()
    ).decode()

    # Compare signatures
    for sig in signature.split(' '):
        version, hash_value = sig.split(',', 1)
        if version != 'v1':
            continue
        if hmac.compare_digest(expected_sig, hash_value):
            return True

    raise ValueError('Invalid signature')
```

## Retry policy

If your endpoint fails to respond with a `2xx` status within 20 seconds, Heffl retries delivery with exponential backoff:

| Attempt | Delay      |
| ------- | ---------- |
| 1       | Immediate  |
| 2       | 5 seconds  |
| 3       | 5 minutes  |
| 4       | 30 minutes |
| 5       | 2 hours    |
| 6       | 5 hours    |
| 7       | 10 hours   |
| 8       | 14 hours   |
| 9       | 20 hours   |
| 10      | 24 hours   |

After 10 failed attempts, the delivery is marked as permanently failed.

### Retry behavior by status code

| Status              | Behavior                                    |
| ------------------- | ------------------------------------------- |
| `2xx`               | Success — no retry                          |
| `410 Gone`          | Endpoint disabled — no further deliveries   |
| `429`, `502`, `504` | Retried                                     |
| Other `4xx`         | Not retried (likely permanent client error) |
| `5xx`               | Retried                                     |

## Best practices

* **Respond quickly.** Return a `200` status immediately and process the event asynchronously. The delivery timeout is 20 seconds.
* **Use idempotency.** The `webhook-id` header is unique per delivery. Store it to detect and skip duplicate deliveries.
* **Verify signatures.** Always validate the `webhook-signature` header to ensure the request is authentic.
* **Check timestamps.** Reject requests with timestamps older than 5 minutes to prevent replay attacks.
* **Use HTTPS.** Always use HTTPS endpoints in production. HTTP endpoints will generate warnings.
* **Handle gracefully.** If you receive an event type you don't recognize, return `200` and ignore it — new events may be added.

## Secret rotation

Heffl supports rotating webhook secrets without downtime. During rotation, signatures are generated with both the old and new secrets (space-separated in the `webhook-signature` header). Your verification code should check if **any** of the signatures is valid.
