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

# Webhooks

> Receive real-time push notifications when key events happen in Sendora.

## Overview

Webhooks let Sendora push events to your server the moment they happen, instead of polling the API. Register an HTTPS endpoint, choose which events you care about, and Sendora will deliver a signed POST request within seconds.

## Event catalog

| Event | Fires when |
| - | - |
| `call.completed` | A voice call finishes, includes transcript URL, duration, outcome |
| `reply.received` | A lead replies on any channel (SMS, email, LinkedIn, WhatsApp) |
| `meeting.booked` | A lead books a meeting (via AI agent or operator in the inbox) |
| `campaign.finished` | Every lead in a campaign completes the sequence |
| `lead.enriched` | An enrichment waterfall run succeeds for a lead |
| `lead.status_changed` | A lead's status changes to `dnc`, `unsubscribed`, or `bounced` |
| `job.completed` | A background job (e.g., bulk enrichment) finishes |

## Registering an endpoint

```bash theme={"dark"}
curl -X POST https://api.sendora.ai/api/v1/public/webhook-endpoints \
  -H "X-API-Key: sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-server.com/hooks/sendora",
    "event_types": ["reply.received", "meeting.booked", "call.completed"],
    "description": "Production webhook for CRM sync"
  }'
```

Returns:

```json theme={"dark"}
{
  "id": "wh_abc123",
  "url": "https://your-server.com/hooks/sendora",
  "secret": "whsec_...",
  "event_types": ["reply.received", "meeting.booked", "call.completed"],
  "is_active": true,
  "created_at": "2026-06-22T10:00:00Z"
}
```

Save the `secret` immediately. It is sensitive and should not be committed or returned to your frontend.

## Payload shape

Every webhook POST contains the event-specific fields plus the `event_type` field:

```json theme={"dark"}
{
  "event_type": "reply.received",
  "lead_id": "lead_abc",
  "channel": "sms",
  "direction": "inbound",
  "message_preview": "Sounds great, I'm interested!",
  "campaign_id": "camp_def",
  "communication_id": "comm_ghi"
}
```

The delivery record ID is available from the endpoint delivery history. The
current request body does not add a universal event ID or timestamp; do not
assume those fields exist unless they are part of the event payload you receive.

### Event-specific payloads

<AccordionGroup>
  <Accordion title="call.completed">
    ```json theme={"dark"}
    {
      "call_id": "call_abc123",
      "lead_id": "lead_xyz",
      "campaign_id": "camp_def",
      "duration_seconds": 187,
      "outcome": "ended",
      "transcript_url": "https://api.sendora.ai/api/v1/recordings/call_abc123"
    }
    ```
  </Accordion>

  <Accordion title="reply.received">
    ```json theme={"dark"}
    {
      "lead_id": "lead_xyz",
      "channel": "email",
      "direction": "inbound",
      "message_preview": "Yes I'd love to learn more...",
      "campaign_id": "camp_def",
      "communication_id": "comm_ghi"
    }
    ```
  </Accordion>

  <Accordion title="meeting.booked">
    ```json theme={"dark"}
    {
      "lead_id": "lead_xyz",
      "campaign_id": "camp_def",
      "meeting_time": "2026-06-25T14:00:00Z",
      "booked_via": "voice_agent"
    }
    ```

    `booked_via` is either `"voice_agent"` (AI booked during a call) or `"operator"` (manual booking in Unibox).
  </Accordion>

  <Accordion title="campaign.finished">
    ```json theme={"dark"}
    {
      "campaign_id": "camp_def",
      "total_leads": 248,
      "completed_at": "2026-06-22T18:30:00Z"
    }
    ```
  </Accordion>

  <Accordion title="lead.enriched">
    ```json theme={"dark"}
    {
      "lead_id": "lead_xyz",
      "fields_added": ["job_title", "company_domain", "linkedin_url"]
    }
    ```
  </Accordion>
</AccordionGroup>

## Verifying signatures

Sendora signs every delivery with HMAC-SHA256 using the endpoint's `secret`. Always verify the signature before processing the payload.

Sendora signs every delivery with HMAC-SHA256. Always verify before processing.

<CodeGroup>
  ```python Python theme={"dark"}
  import hmac, hashlib

  def verify(payload_bytes: bytes, signature_header: str, secret: str) -> bool:
      expected = hmac.new(secret.encode(), payload_bytes, hashlib.sha256).hexdigest()
      return hmac.compare_digest(f"sha256={expected}", signature_header)
  ```

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

  function verify(payloadBuffer, signatureHeader, secret) {
    const expected = crypto
      .createHmac("sha256", secret)
      .update(payloadBuffer)
      .digest("hex");
    return crypto.timingSafeEqual(
      Buffer.from(`sha256=${expected}`),
      Buffer.from(signatureHeader)
    );
  }
  ```

  ```bash cURL test theme={"dark"}
  # Compute expected signature manually for testing
  echo -n '{"id":"evt_..."}' | openssl dgst -sha256 -hmac "whsec_your_secret"
  ```
</CodeGroup>

The signature arrives in the `X-Sendora-Signature` header as `sha256=<hex>`. Reject anything that doesn't match.

Here's how to wire it into a request handler:

```python theme={"dark"}
@app.post("/hooks/sendora")
async def handle(request: Request):
    body = await request.body()
    sig = request.headers.get("X-Sendora-Signature", "")
    if not verify(body, sig, WEBHOOK_SECRET):
        return Response(status_code=401)
    event = await request.json()
    # process event...
    return {"ok": True}
```

<Warning>
  Never process a webhook payload without verifying the signature. Forged events are rejected as 401 on Sendora's side, but your own endpoint must also verify.
</Warning>

## Delivery flow

```mermaid theme={"dark"}
sequenceDiagram
    participant S as Sendora
    participant T as Background processing
    participant Y as Your Server
    S->>S: Event fires (reply, call, meeting...)
    S->>T: Enqueue DeliverWebhookWorkflow
    T->>Y: POST /your-endpoint (signed payload)
    alt 2xx response
        Y-->>T: 200 OK
        T->>S: Mark delivery succeeded
    else non-2xx or timeout
        Y-->>T: 500 / timeout
        T->>T: Retry after 10s
        T->>T: Retry after 100s
        T->>S: Mark delivery failed after 3 attempts
    end
```

## Delivery and retries

* Sendora expects your endpoint to return `2xx` within **30 seconds**
* If it times out or returns a non-2xx, Sendora retries after 10 seconds and 100 seconds (3 attempts total)
* After 5 failures, the delivery is marked `failed`, check the delivery log

## Viewing delivery history

```bash theme={"dark"}
GET /api/v1/public/webhook-endpoints/{endpoint_id}/deliveries
```

Returns a list of recent delivery attempts with status codes, durations, and error messages.

## Testing an endpoint

Send a synthetic `call.completed` event to verify your endpoint is reachable:

```bash theme={"dark"}
POST /api/v1/public/webhook-endpoints/{endpoint_id}/test
```

Your endpoint will receive a synthetic `call.completed` test payload.

## Best practices

1. **Acknowledge fast, process async**, return `200` immediately, then handle the event in a queue/worker
2. **Make handlers idempotent**, the same delivery may be attempted more than once; use a stable event-specific identifier when one is present, or deduplicate using your own delivery ledger
3. **Always verify signatures**, reject anything with an invalid or missing signature header
4. **Monitor the delivery log**, set up alerts if failure rate climbs


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