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

# Webhooks

> Customer-registered webhooks are not yet available. Use polling endpoints for campaign state, replies, and email events.

<Warning>
  **Bavlio does not yet ship a customer-facing outbound webhook system.** You cannot register a URL and have Bavlio POST events to it. Until that ships, poll the endpoints below.
</Warning>

## Why "not yet"?

A safe outbound webhook system requires SSRF defense at both registration and dispatch, retry semantics, dead-letter handling, and signing. Shipping it half-built is worse than shipping it later. The full design is parked in the backend roadmap.

## Use these polling endpoints instead

| Method | Path                                                | Purpose                                                       |
| ------ | --------------------------------------------------- | ------------------------------------------------------------- |
| `GET`  | `/api/v1/campaigns/{campaign_id}/stats`             | Comprehensive campaign stats (sends, opens, clicks, replies). |
| `GET`  | `/api/v1/campaigns/{campaign_id}/leads/{id}/events` | Per-lead email event timeline.                                |
| `GET`  | `/api/v1/bavimail/emails`                           | List sent emails with status (delivered, opened, bounced).    |
| `GET`  | `/api/v1/bavimail/inbound-emails`                   | List inbound replies.                                         |
| `GET`  | `/api/v1/bavimail/conversations`                    | Threaded inbound + outbound view.                             |

## Poll inbound replies

Track which message ids you've seen and process new ones. Persist seen ids across restarts in production.

```python Python theme={null}
import time, httpx, os

client = httpx.Client(
    base_url="https://api.bavlio.com",
    headers={"Authorization": f"Bearer {os.environ['BAVLIO_API_KEY']}"},
    timeout=30.0,
)

seen_ids = set()
while True:
    response = client.get(
        "/api/v1/bavimail/inbound-emails",
        params={"limit": 100},
    )
    response.raise_for_status()
    for msg in response.json():
        if msg["id"] in seen_ids:
            continue
        seen_ids.add(msg["id"])
        # Handle the new reply
        print(f"Reply from {msg.get('from_email')}: {msg.get('subject')}")
    time.sleep(60)  # poll every minute
```

## Poll campaign completion

```python Python theme={null}
import time, httpx, os

client = httpx.Client(
    base_url="https://api.bavlio.com",
    headers={"Authorization": f"Bearer {os.environ['BAVLIO_API_KEY']}"},
    timeout=30.0,
)

campaign_id = "campaign_abc123"
while True:
    stats = client.get(f"/api/v1/campaigns/{campaign_id}/stats").json()
    if stats.get("queued", 0) == 0 and stats.get("in_flight", 0) == 0:
        print("Campaign complete:", stats)
        break
    print("Pending sends:", stats.get("queued", 0))
    time.sleep(120)
```

## Internal webhook receivers (context only)

For visibility: Bavlio operates three inbound webhook receivers — one each for BaviMail, Unipile (LinkedIn), and Stripe. These are server-to-server endpoints between Bavlio and our infrastructure providers, not points you can register against. They are documented internally and surfaced here only so agents understand the data flow.

## Roadmap

Once the customer webhook system ships, this page will document: registration endpoint, signing scheme (HMAC-SHA256 with `timestamp.body` canonical), 5-minute replay window, retry policy with dead-letter queue, and a verification snippet you can drop into any framework.

<Card title="← Back to Quickstart" icon="rocket" href="/quickstart" />
