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

# Webhooks

> Get a signed POST to your server when a call or a batch finishes.

A webhook sends the result of a call to your server when the call ends. You do not need to poll.

There are two events:

| Event | Sent when |
| - | - |
| `call.completed` | A phone call or a web session ends. |
| `batch.completed` | A batch reaches a final state. |

<Note>
  Registered webhooks also get `call.completed` when a web session ends. In that payload, `call_sid` and `to_number` are `null`. Use `conversation_id` to identify the session. The per-call `webhook_url` field exists only on phone calls and batches.
</Note>

## Add a webhook

You can add a webhook in two ways.

<Tabs>
  <Tab title="For every call">
    Register a URL once. It gets every matching event in your workspace until you delete it.

    ```bash theme={"system"}
    curl -X POST https://app.eclatira.com/api/v1/webhooks \
      -H "Authorization: Bearer $ECLATIRA_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "url": "https://example.com/eclatira/webhook",
        "events": ["call.completed"]
      }'
    ```

    * If you leave out `events`, the URL gets both events.
    * If you register the same URL again, you get the existing webhook back. No duplicate is created.
    * A workspace can have up to **20** webhooks.
  </Tab>

  <Tab title="For one call">
    Add `webhook_url` when you create a call or a batch. Only that call or batch is sent to it.

    ```json theme={"system"}
    {
      "agent_id": "your-agent-id",
      "to_number": "+15551234567",
      "webhook_url": "https://example.com/eclatira/webhook"
    }
    ```
  </Tab>
</Tabs>

If a URL is registered and also passed as `webhook_url`, it gets the event only once.

## Payloads

<Tabs>
  <Tab title="call.completed">
    ```json theme={"system"}
    {
      "event": "call.completed",
      "conversation_id": "conv_8f2a...",
      "call_sid": "CAc29a9cb3a1dc3e077f4a9a25f5161e4d",
      "agent_id": "your-agent-id",
      "to_number": "+15551234567",
      "provider": "twilio",
      "first_message": null,
      "first_message_delivered": true,
      "variables": { "first_name": "Maria" },
      "duration": 94,
      "status": "completed",
      "message_count": 12,
      "transcript": [
        { "role": "model", "text": "Hi Maria, this is Sam from Acme.", "timestamp": 1790000100.2 },
        { "role": "user", "text": "Hi Sam.", "timestamp": 1790000102.8 }
      ],
      "summary": "Maria confirmed her appointment for Friday.",
      "sentiment": "positive",
      "goal_met": true,
      "needs_review": false,
      "unanswered_questions": [],
      "interruption_count": 1,
      "latency_avg_ms": 640,
      "created_at": "2026-09-21T10:15:00+00:00"
    }
    ```

    In `transcript`, `role` is `model` for the agent and `user` for the person. `timestamp` is a Unix time in seconds. If the agent called any tools, the payload also has a `tool_calls` list.
  </Tab>

  <Tab title="batch.completed">
    ```json theme={"system"}
    {
      "event": "batch.completed",
      "batch_id": "b_4d1e...",
      "agent_id": "your-agent-id",
      "name": "Appointment reminders",
      "status": "completed",
      "total": 250,
      "completed_count": 231,
      "failed_count": 7,
      "voicemail_count": 12,
      "created_at": "2026-09-21T09:00:00+00:00",
      "completed_at": "2026-09-21T10:42:10+00:00"
    }
    ```

    The batch payload has counts only. To see each recipient, call `GET /api/v1/batches/{batch_id}`.
  </Tab>
</Tabs>

The values above are examples. The field names are exact.

## Verify the signature

Every webhook has an `X-Webhook-Signature` header. It is an HMAC-SHA256 of the raw request body, hex-encoded, signed with your workspace's webhook secret.

<Steps>
  <Step title="Get your secret">
    ```bash theme={"system"}
    curl https://app.eclatira.com/api/v1/webhooks/secret \
      -H "Authorization: Bearer $ECLATIRA_API_KEY"
    ```

    The response is `{"webhook_secret": "..."}`. You can fetch it again at any time.
  </Step>

  <Step title="Sign the raw body">
    Compute the HMAC over the **exact bytes** you received. Do not parse the JSON and convert it back to a string first. That can change the bytes and break the check.
  </Step>

  <Step title="Compare in constant time">
    Use a constant-time compare, such as `hmac.compare_digest` or `crypto.timingSafeEqual`. A normal `==` can leak timing information.
  </Step>
</Steps>

<CodeGroup>
  ```python Python (FastAPI) theme={"system"}
  import hashlib
  import hmac
  import os

  from fastapi import FastAPI, HTTPException, Request

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


  @app.post("/eclatira/webhook")
  async def eclatira_webhook(request: Request) -> dict:
      raw_body = await request.body()
      signature = request.headers.get("X-Webhook-Signature", "")
      expected = hmac.new(SECRET.encode(), raw_body, hashlib.sha256).hexdigest()
      if not hmac.compare_digest(expected, signature):
          raise HTTPException(status_code=401)

      event = await request.json()
      print(event["event"], event.get("summary"))
      return {"ok": True}
  ```

  ```js Node.js (Express) theme={"system"}
  import crypto from "node:crypto";
  import express from "express";

  const app = express();
  const SECRET = process.env.ECLATIRA_WEBHOOK_SECRET;

  // express.raw keeps the body as the exact bytes that were signed.
  app.post("/eclatira/webhook", express.raw({ type: "application/json" }), (req, res) => {
    const expected = crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
    const received = req.get("X-Webhook-Signature") ?? "";

    const valid =
      received.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
    if (!valid) return res.sendStatus(401);

    const event = JSON.parse(req.body.toString("utf8"));
    console.log(event.event, event.summary);
    res.sendStatus(200);
  });

  app.listen(3000);
  ```
</CodeGroup>

## Delivery

Know these rules before you depend on webhooks:

* **One attempt only.** If your server is down or returns an error, the webhook is not sent again. Use `GET /api/v1/calls/{call_id}` to fill any gaps.
* **10 second timeout.** Reply quickly. Do slow work after you respond.
* **Redirects are not followed.** Give the final URL.
* **It can arrive unsigned.** In rare cases the secret cannot be loaded, and the webhook is sent without a signature. Reject any request with a missing or wrong signature.

## Manage webhooks

| Action | Endpoint |
| - | - |
| List webhooks | `GET /api/v1/webhooks` |
| Delete a webhook | `DELETE /api/v1/webhooks/{webhook_id}` |
| Get the signing secret | `GET /api/v1/webhooks/secret` |

See the [Webhooks API reference](/api-reference/webhooks/create-a-webhook) for full schemas.


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