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

# Webhooks Overview

> Understand how Truedy webhooks work, when to use them, and how to set one up in 5 minutes

# Webhooks Overview

Webhooks let Truedy **push events to your server in real time** when important things happen — a call ends, a batch campaign completes, a voice clone finishes training. Instead of polling the API every few seconds, your server gets notified the moment the event occurs.

## When to use webhooks

| Use case                                         | Webhook to use                |
| ------------------------------------------------ | ----------------------------- |
| Update your CRM when a call completes            | `call.ended`                  |
| Send a follow-up email after an outbound call    | `call.ended`                  |
| Trigger a workflow when a campaign finishes      | `batch.completed`             |
| Monitor live call activity in your ops dashboard | `call.started`, `call.joined` |
| Alert on call failures                           | `call.failed`                 |
| Enable a voice once training is done             | `voice.training.completed`    |

Without webhooks you'd need to poll the API constantly — webhooks are more reliable, cheaper, and real-time.

## How it works

<Steps>
  <Step title="You create a webhook endpoint in your app">
    A publicly accessible HTTPS URL that accepts `POST` requests with a JSON body.
  </Step>

  <Step title="You register the endpoint in Truedy">
    Dashboard → **Settings → Webhooks → Add Endpoint**. Paste your URL and choose which event types to receive.
  </Step>

  <Step title="An event occurs">
    A call ends, a campaign completes, etc.
  </Step>

  <Step title="Truedy dispatches the event">
    Truedy bundles the event data, signs it with HMAC-SHA256, and sends an HTTP `POST` to your URL.
  </Step>

  <Step title="Your server processes the event">
    Verify the signature, extract the event type and data, run your business logic.
  </Step>

  <Step title="Return 204 quickly">
    Truedy expects a `2xx` response. If it doesn't receive one within the timeout window, it may re-deliver the event. Return `204` immediately — process asynchronously if needed.
  </Step>
</Steps>

## Minimal working receiver

Here is a minimal webhook handler to get you started:

<CodeGroup>
  ```typescript Node.js / TypeScript theme={null}
  import express from 'express'
  import crypto from 'crypto'

  const app = express()
  const SECRET = process.env.TRUEDY_WEBHOOK_SECRET!

  // express.raw() preserves the exact bytes needed for HMAC verification
  app.post('/webhooks/truedy', express.raw({ type: 'application/json' }), (req, res) => {
    const rawBody = req.body as Buffer
    const timestamp = req.headers['x-truedy-timestamp'] as string
    const signature = req.headers['x-truedy-signature'] as string

    // 1. Verify signature
    const msg = `${timestamp}.${rawBody.toString('utf8')}`
    const expected = crypto.createHmac('sha256', SECRET).update(msg).digest('hex')
    const now = Math.floor(Date.now() / 1000)
    const ts = parseInt(timestamp, 10)

    if (isNaN(ts) || now - ts > 300 || !crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(signature, 'hex'))) {
      return res.status(401).json({ error: 'Invalid signature' })
    }

    // 2. Parse and handle
    const event = JSON.parse(rawBody.toString('utf8'))
    console.log('Received:', event.event)

    switch (event.event) {
      case 'call.ended':
        // event.data.call.callId, event.data.call.duration, event.data.call.summary
        break
      case 'batch.completed':
        // event.data.batch_id
        break
    }

    res.status(204).send()
  })

  app.listen(3000)
  ```

  ```python Python (FastAPI) theme={null}
  import hashlib, hmac, json, os, time
  from fastapi import FastAPI, Request, HTTPException
  from fastapi.responses import Response

  app = FastAPI()
  SECRET = os.environ["TRUEDY_WEBHOOK_SECRET"]

  @app.post("/webhooks/truedy")
  async def handle(request: Request):
      raw = await request.body()
      ts = request.headers.get("x-truedy-timestamp", "")
      sig = request.headers.get("x-truedy-signature", "")

      # 1. Verify
      now = int(time.time())
      try:
          t = int(ts)
      except ValueError:
          raise HTTPException(401, "Bad timestamp")
      if now - t > 300:
          raise HTTPException(401, "Stale timestamp")

      expected = hmac.new(SECRET.encode(), f"{ts}.{raw.decode()}".encode(), hashlib.sha256).hexdigest()
      if not hmac.compare_digest(expected, sig):
          raise HTTPException(401, "Invalid signature")

      # 2. Handle
      event = json.loads(raw)
      print("Received:", event["event"])

      if event["event"] == "call.ended":
          call = event["data"].get("call", {})
          print(f"  call_id={call.get('callId')} duration={call.get('duration')}s")

      return Response(status_code=204)
  ```
</CodeGroup>

## Event payload structure

Every event Truedy sends has this envelope:

```json theme={null}
{
  "event": "call.ended",
  "timestamp": "2026-03-18T14:32:00.000Z",
  "data": {
    "call": {
      "callId": "uv_call_abc123",
      "agent": { "agentId": "uv_agent_xyz" },
      "duration": 142,
      "costUsd": 0.21,
      "summary": "Caller asked about pricing, shown the Pro plan.",
      "endReason": "completed"
    }
  }
}
```

| Field       | Description                                             |
| ----------- | ------------------------------------------------------- |
| `event`     | Event type string, e.g. `call.ended`                    |
| `timestamp` | ISO 8601 timestamp when the event was dispatched        |
| `data`      | Event-specific payload — structure varies by event type |

See [Available Webhooks](/guides/available-webhooks) for the full catalogue of event types and their payload shapes.

## Reliability guarantees

<Note>
  Truedy delivers webhooks **at-least-once**. Your receiver must be idempotent — the same event may arrive more than once if a delivery fails or times out.
</Note>

To handle duplicates safely:

1. Extract a **dedupe key** from the payload (`data.call.callId`, `data.batch_id`, etc.)
2. Before processing, check whether that key is already in your database
3. If yes: return `204` immediately
4. If no: process, then record the key atomically

Full runbook in [Webhook Error Handling & Retries](/guides/webhook-error-handling-and-retries).

## Event families

| Family              | Events                                                                    |
| ------------------- | ------------------------------------------------------------------------- |
| **Calls**           | `call.started`, `call.joined`, `call.ended`, `call.failed`, `call.billed` |
| **Batch/Campaigns** | `batch.status.changed`, `batch.completed`                                 |
| **Voice training**  | `voice.training.completed`, `voice.training.failed`                       |

## Next steps

<Columns>
  <Card title="Available Webhooks" icon="list" href="/guides/available-webhooks">
    Complete event catalogue with payload shapes
  </Card>

  <Card title="Securing Webhooks" icon="shield" href="/guides/securing-webhooks">
    Node.js, Python & Flask HMAC verification examples
  </Card>

  <Card title="Error Handling & Retries" icon="rotate" href="/guides/webhook-error-handling-and-retries">
    Idempotent receiver implementation with DB example
  </Card>

  <Card title="Idempotency" icon="check" href="/guides/idempotency">
    Platform-wide idempotency patterns
  </Card>
</Columns>
