# Webhooks

Signed JSON events Covo sends to your URL (and to Zapier, Make or n8n), how to verify them, and the booking webhooks Covo receives.

Set webhooks up in Studio: Settings, Integrations, Webhook. Covo generates a signing secret and shows it once.

## Events

- `lead.created`
- `lead.qualified`
- `lead.stage_changed`
- `booking.created`
- `agent.configuration.updated`
- `agent.version.created`
- `agent.version.restored`
- `agent.draft.applied`
- `agent.test.completed`
- `agent.published`
- `agent.unpublished`

## What Covo sends

```http
POST https://your-endpoint.example/hook
Content-Type: application/json
User-Agent: Covo-Webhooks/1.0
X-Covo-Event: lead.created
X-Covo-Delivery: 7d0f...
X-Covo-Timestamp: 1791403629
X-Covo-Signature: sha256=5c2b...

{
  "id": "7d0f...",
  "event": "lead.created",
  "createdAt": "2026-10-07T18:20:29.000Z",
  "tenant": { "id": "...", "slug": "avery", "name": "Avery Quinn" },
  "data": { "lead": { "id": "...", "name": "Rae Banks", "email": "rae@banks.example", "stage": "lead", "score": 42 } }
}
```

The same headers are also sent with the older `X-Concierge-` prefix, for receivers built before the rename. `id` (and `X-Covo-Delivery`) stays the same across retries: use it to ignore duplicates.

## Verify the signature

`X-Covo-Signature` is `sha256=` followed by the hex HMAC-SHA256 of `{timestamp}.{raw body}`, keyed with your secret. Compute it over the raw body (before parsing), compare in constant time, and reject timestamps more than 5 minutes old.

Node.js:

```javascript
import crypto from 'node:crypto';

export function verifyCovo(rawBody, headers, secret) {
  const ts = headers['x-covo-timestamp'];
  const sig = headers['x-covo-signature'] ?? '';
  if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex');
  return sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
```

Python:

```python
import hashlib, hmac, time

def verify_covo(raw_body: bytes, headers: dict, secret: str) -> bool:
    ts = headers.get("x-covo-timestamp", "")
    sig = headers.get("x-covo-signature", "")
    if not ts or abs(time.time() - int(ts)) > 300:
        return False
    expected = "sha256=" + hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(sig, expected)
```

## Zapier, Make and n8n

- **Zapier**: use the "Webhooks by Zapier" trigger, Catch Hook (a paid Zapier feature). To check signatures, add a Code by Zapier step with the Node.js check above.
- **Make**: add a Custom webhook module. Check the signature with Make's `sha256` function using your secret as the key, or a Code module.
- **n8n**: add a Webhook node (POST), copy the Production URL and activate the workflow (the test URL only works while you are testing). Check the signature with the Crypto node (HMAC, SHA256, hex).

## Retries

A delivery that fails with a network error, a timeout, `408`, `429` or `5xx` is retried up to 6 times over about an hour. Other `4xx` answers mean the address or setup is wrong and are not retried. Studio shows every attempt in the integration's delivery history.

## Booking webhooks Covo receives

Cal.com and Calendly tell Covo about bookings, so the person's lead moves to Opportunity (or is created).

- **Cal.com**: Covo shows a webhook URL and secret. In Cal.com open Settings, Developer, Webhooks, New; paste both; turn on Booking created, rescheduled and cancelled (and requested, if bookings need confirmation); keep the default payload. Covo checks `x-cal-signature-256` (HMAC-SHA256 of the raw body).
- **Calendly**: needs a paid Calendly plan. Paste a Calendly personal access token in Studio and Covo creates the webhook subscription for you (Calendly has no screen for it), signed with a key Covo generates. Covo checks `Calendly-Webhook-Signature` (HMAC-SHA256 of `t.body`, 5 minute tolerance).
- The same signed delivery is accepted once; repeats are acknowledged and ignored.
