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

# Webhooks

> Receive real-time, signed notifications when events occur in your Quarterzip workspace.

<Warning>
  **Preview feature.** Webhooks are currently in preview. Seamless delivery is not guaranteed, and the API, payload shape, and behaviour are likely to change in future releases.
</Warning>

Quarterzip can notify your systems in real time when an event happens in your workspace. You register an HTTPS endpoint and Quarterzip sends a signed `POST` request to it whenever a relevant event occurs.

Today there is a single event — `call.completed` — sent once a call has finished and its transcript and ratings are available.

## Set up an endpoint

1. In the Quarterzip dashboard, go to **Settings → Webhooks**.
2. Enter the **HTTPS** URL that should receive events.
3. Toggle **Enabled** on and save.

When you save a URL for the first time, Quarterzip generates a **signing secret** and displays it **once**. Copy it immediately and store it somewhere secure, such as a secrets manager — never in source control. You'll need it to verify that incoming requests genuinely came from Quarterzip, and it will not be shown again.

<Note>
  The endpoint URL must use `https` and must resolve to a public IP address. Localhost, private-network, and link-local addresses are rejected.
</Note>

You can configure **one** endpoint per workspace.

### Rotating the secret

If a secret is ever leaked, click **Rotate secret** on the same settings page. A new secret is generated and shown once.

<Warning>
  Rotation takes effect immediately. The old secret stops verifying as soon as you rotate, so update your endpoint with the new value promptly to avoid rejecting deliveries in between.
</Warning>

## The `call.completed` event

Quarterzip sends this event after a call finishes processing. The request is a JSON `POST` with `Content-Type: application/json`.

### Example payload

```json theme={null}
{
  "type": "call.completed",
  "timestamp": "2026-06-19T14:32:05.123456+00:00",
  "data": {
    "call_id": "abc123",
    "workspace_id": "ws_456",
    "deployment_id": "dep_789",
    "started_at": "2026-06-19T14:28:51.000000+00:00",
    "ended_at": "2026-06-19T14:31:48.000000+00:00",
    "is_test": false,
    "is_coaching": false,
    "is_phone": true,
    "is_sdk": false,
    "call_quality_rating": 5,
    "product_experience_rating": 4,
    "transcript": [
      { "speaker": "agent", "text": "Hi, thanks for calling — how can I help?" },
      { "speaker": "user", "text": "I'd like to update my billing address." }
    ]
  }
}
```

### Fields

| Field                            | Type                      | Notes                                                                                                                                        |
| -------------------------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`                           | string                    | Always `call.completed` for this event.                                                                                                      |
| `timestamp`                      | string (ISO 8601)         | When the event was sent. Also available as a Unix timestamp in the `webhook-timestamp` header.                                               |
| `data.call_id`                   | string                    | Unique ID of the call.                                                                                                                       |
| `data.workspace_id`              | string                    | The workspace the call belongs to.                                                                                                           |
| `data.deployment_id`             | string                    | The agent deployment the call used.                                                                                                          |
| `data.started_at`                | string (ISO 8601)         | When the call started.                                                                                                                       |
| `data.ended_at`                  | string (ISO 8601) \| null | When the call ended. `null` if not recorded.                                                                                                 |
| `data.is_test`                   | boolean                   | `true` for test calls.                                                                                                                       |
| `data.is_coaching`               | boolean                   | `true` for coaching calls.                                                                                                                   |
| `data.is_phone`                  | boolean                   | `true` if the call came in over the phone.                                                                                                   |
| `data.is_sdk`                    | boolean                   | `true` if the call was placed through the embedded SDK.                                                                                      |
| `data.call_quality_rating`       | integer \| null           | End-user call-quality rating, if collected.                                                                                                  |
| `data.product_experience_rating` | integer \| null           | End-user product-experience rating, if collected.                                                                                            |
| `data.transcript`                | array                     | Ordered turns, each `{ "speaker": string, "text": string }`. May be empty — see [truncation](#payload-size-and-transcript-truncation) below. |

### Which calls trigger a webhook

* **Test** and **coaching** calls **are** delivered. Use the `is_test` and `is_coaching` flags to filter them out if you only care about real traffic.

### Payload size and transcript truncation

Payloads are capped at **1 MB**. If a call's transcript would push the payload over that limit, Quarterzip drops the transcript and adds a flag so you can detect it:

```json theme={null}
{
  "type": "call.completed",
  "data": {
    "call_id": "abc123",
    "transcript": [],
    "transcript_truncated": true
  }
}
```

## Verify the signature

<Warning>
  Always verify the signature before trusting a webhook. It proves the request came from Quarterzip and wasn't tampered with in transit.
</Warning>

Quarterzip signs every delivery using the open [Standard Webhooks](https://www.standardwebhooks.com/) specification, so you can use the official `standardwebhooks` libraries to verify with a few lines of code.

Each request includes three headers:

| Header              | Description                                                         |
| ------------------- | ------------------------------------------------------------------- |
| `webhook-id`        | Unique ID for this delivery (for example, `call_completed_abc123`). |
| `webhook-timestamp` | Send time as a Unix timestamp (seconds).                            |
| `webhook-signature` | Space-separated list of `v1,<base64-signature>` values.             |

<Note>
  Verify against the raw request body, exactly as received. Do not parse and re-serialize the JSON first, or the signature won't match.
</Note>

<CodeGroup>
  ```python Python theme={null}
  # pip install standardwebhooks
  from standardwebhooks import Webhook

  # The whole "whsec_..." string shown when you created or rotated the secret.
  WEBHOOK_SECRET = "whsec_..."

  def handle_webhook(raw_body: bytes, headers: dict[str, str]) -> None:
      wh = Webhook(WEBHOOK_SECRET)

      # Raises on an invalid signature or an out-of-tolerance timestamp.
      payload = wh.verify(raw_body, headers)

      if payload["type"] == "call.completed":
          process_completed_call(payload["data"])
  ```

  ```ts Node / TypeScript theme={null}
  // npm install standardwebhooks
  import { Webhook } from 'standardwebhooks';

  // The whole "whsec_..." string shown when you created or rotated the secret.
  const WEBHOOK_SECRET = 'whsec_...';

  // `rawBody` must be the exact bytes/string received, not a re-serialized object.
  function handleWebhook(rawBody: string, headers: Record<string, string>) {
    const wh = new Webhook(WEBHOOK_SECRET);

    // Throws on an invalid signature or an out-of-tolerance timestamp.
    const payload = wh.verify(rawBody, headers) as { type: string; data: unknown };

    if (payload.type === 'call.completed') {
      processCompletedCall(payload.data);
    }
  }
  ```
</CodeGroup>

The library also enforces a timestamp tolerance, which protects you against replayed deliveries.

## Troubleshooting

| Symptom                      | Likely cause                                                                                              |
| ---------------------------- | --------------------------------------------------------------------------------------------------------- |
| Not receiving any events     | Endpoint is not **Enabled**, or no real calls are completing.                                             |
| Signature verification fails | Verifying against a re-serialized body instead of the raw bytes, or using an old secret after a rotation. |
| Can't save the URL           | URL must be `https` and resolve to a public IP — private and localhost addresses are rejected.            |
| Lost the signing secret      | The secret is only shown once — rotate it from **Settings → Webhooks** to generate a new one.             |
