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

# Webhooks

> Subscribe to Zeeg webhooks to receive real-time notifications when events are scheduled, rescheduled, cancelled, or when routing forms are submitted.

Webhooks let you receive real-time HTTP POST notifications when events happen in Zeeg. Instead of polling the API, you register a callback URL and Zeeg pushes event data to you the moment something occurs.

Zeeg supports four event types:

| Event                                                         | Trigger                                       |
| ------------------------------------------------------------- | --------------------------------------------- |
| `invitee.scheduled`                                           | An invitee books, or a booking is rescheduled |
| `invitee.cancelled`                                           | An invitee or host cancels a booking          |
| `routing_form.submitted`                                      | A visitor submits a routing form              |
| `ai_agent.call_completed` <Badge color="orange">Alpha</Badge> | An AI agent call reaches a terminal state     |

## Setting up webhooks

Create a webhook subscription by sending a `POST` request to `/webhooks`.

### Required fields

| Field         | Type   | Description                                                   |
| ------------- | ------ | ------------------------------------------------------------- |
| `callbackUrl` | string | The URL Zeeg will send events to. Must be publicly reachable. |
| `events`      | array  | One or more event types to subscribe to.                      |
| `scope`       | string | `user` or `organization`.                                     |

### Optional fields

| Field   | Type   | Description                                                   |
| ------- | ------ | ------------------------------------------------------------- |
| `token` | string | A secret token included in delivery headers for verification. |

### Scope explained

* **`user`** — You only receive events related to your own scheduling pages.
* **`organization`** — You receive events for all members of your organization. Requires an API token belonging to an admin or owner.

### Example

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.zeeg.me/v2/webhooks \
    -H "Authorization: Bearer YOUR_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "callbackUrl": "https://example.com/webhooks/zeeg",
      "events": ["invitee.scheduled", "invitee.cancelled"],
      "scope": "organization",
      "token": "my-secret-verification-token"
    }'
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      "https://api.zeeg.me/v2/webhooks",
      headers={
          "Authorization": "Bearer YOUR_TOKEN",
          "Content-Type": "application/json",
      },
      json={
          "callbackUrl": "https://example.com/webhooks/zeeg",
          "events": ["invitee.scheduled", "invitee.cancelled"],
          "scope": "organization",
          "token": "my-secret-verification-token",
      },
  )

  print(response.json())
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch("https://api.zeeg.me/v2/webhooks", {
    method: "POST",
    headers: {
      Authorization: "Bearer YOUR_TOKEN",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      callbackUrl: "https://example.com/webhooks/zeeg",
      events: ["invitee.scheduled", "invitee.cancelled"],
      scope: "organization",
      token: "my-secret-verification-token",
    }),
  });

  const data = await response.json();
  console.log(data);
  ```

  ```php PHP theme={null}
  $ch = curl_init("https://api.zeeg.me/v2/webhooks");

  curl_setopt_array($ch, [
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_POST => true,
      CURLOPT_HTTPHEADER => [
          "Authorization: Bearer YOUR_TOKEN",
          "Content-Type: application/json",
      ],
      CURLOPT_POSTFIELDS => json_encode([
          "callbackUrl" => "https://example.com/webhooks/zeeg",
          "events" => ["invitee.scheduled", "invitee.cancelled"],
          "scope" => "organization",
          "token" => "my-secret-verification-token",
      ]),
  ]);

  $response = curl_exec($ch);
  curl_close($ch);

  echo $response;
  ```
</CodeGroup>

### Response

```json theme={null}
{
  "success": true,
  "resource": {
    "uuid": "9a39bf60-a6c3-45e7-80cd-2cd36e520861",
    "name": null,
    "description": null,
    "callbackUrl": "https://example.com/webhooks/zeeg",
    "scope": "organization",
    "creator": {
      "firstName": "Lena",
      "lastName": "Meier",
      "slug": "lena-meier"
    },
    "events": ["invitee.scheduled", "invitee.cancelled"],
    "organization": null,
    "apiVersion": null,
    "createdAt": "2026-04-10T08:30:00.000000Z",
    "updatedAt": "2026-04-10T08:30:00.000000Z"
  }
}
```

## Verifying webhook origin

If you provide a `token` when creating the webhook subscription, Zeeg sends it in the `Token` header of every webhook request. Use this value to verify that incoming requests actually come from Zeeg and not a third party.

```python Python theme={null}
from flask import Flask, request, abort

WEBHOOK_TOKEN = "my-secret-verification-token"

app = Flask(__name__)

@app.route("/webhooks/zeeg", methods=["POST"])
def handle_webhook():
    token = request.headers.get("Token")
    if token != WEBHOOK_TOKEN:
        abort(403)

    payload = request.json
    # Process the event
    return "", 200
```

## Event payloads

### invitee.scheduled / invitee.cancelled

Both event types share the same payload structure. Cancellation-specific and reschedule-specific fields are populated only when relevant.

```json theme={null}
{
  "event": "invitee.scheduled",
  "eventUri": "https://api.zeeg.me/v2/scheduled-events/zg-O69bac566950c6",
  "inviteeSalutation": "Ms.",
  "inviteeFirstName": "Sophie",
  "inviteeLastName": "Laurent",
  "inviteeName": "Sophie Laurent",
  "inviteeEmail": "sophie.laurent@northwind.io",
  "inviteePhoneNumber": "+49 170 9876543",
  "inviteeTimezone": "Europe/Paris",
  "inviteeNumberOfSeats": 1,
  "teamName": null,
  "hosts": {
    "1": "Lena Meier"
  },
  "hostsDetails": [
    {
      "slug": "lena-meier",
      "firstName": "Lena",
      "lastName": "Meier",
      "email": "lena.meier@horizondigital.de",
      "fullName": "Lena Meier"
    }
  ],
  "title": "30-Minute Discovery Call",
  "duration": 30,
  "durationPretty": "30 mins",
  "type": "ONE_ON_ONE",
  "eventTypeUri": "https://api.zeeg.me/v2/event-types/80f46bf5-eb01-4c07-960e-a9a3e18aae5e",
  "singleUseLink": null,
  "startAt": "2026-04-15T09:00:00+00:00",
  "inviteeStartAt": "2026-04-15T11:00:00+02:00",
  "inviteeStartAtPretty": "11:00 - Wednesday, April 15, 2026",
  "endAt": "2026-04-15T09:30:00+00:00",
  "inviteeEndAt": "2026-04-15T11:30:00+02:00",
  "inviteeEndAtPretty": "11:30 - Wednesday, April 15, 2026",
  "location": "Google Meet",
  "locationLink": "https://meet.google.com/abc-defg-hij",
  "questions": [
    {
      "question": "What would you like to discuss?",
      "answer": "Product demo and pricing options"
    }
  ],
  "questionsAndAnswers": {
    "What would you like to discuss?": "Product demo and pricing options"
  },
  "price": null,
  "pricePerSeat": null,
  "currency": null,
  "paymentGateway": null,
  "transactionId": null,
  "guests": ["alex.chen@northwind.io"],
  "eventUuid": "zg-O69bac566950c6",
  "inviteeUuid": "zg-O69bad4047abf0",
  "cancelUrl": "https://zeeg.me/cancel/zg-O69bad4047abf0",
  "rescheduleUrl": "https://zeeg.me/rescheduling/zg-O69bad4047abf0",
  "cancelled": false,
  "cancelledAt": null,
  "cancelledBy": null,
  "cancellationReason": null,
  "rescheduled": false,
  "rescheduledAt": null,
  "rescheduledBy": null,
  "rescheduleReason": null,
  "oldEventUri": null,
  "oldEventUuid": null,
  "oldInviteeUuid": null,
  "oldStartAt": null,
  "oldInviteeStartAt": null,
  "newEventUri": null,
  "newEventUuid": null,
  "newInviteeUuid": null,
  "newStartAt": null,
  "newInviteeStartAt": null,
  "utmCampaign": "spring_launch",
  "utmSource": "linkedin",
  "utmMedium": "social",
  "utmTerm": null,
  "utmContent": null,
  "adAttribution": {
    "gclid": "Cj0KCQjw_ndBhCrARIsAAy",
    "fbclid": "IwAR2xq9Zt",
    "landingUrl": "https://zeeg.me/lena-meier/30min?gclid=Cj0KCQjw_ndBhCrARIsAAy"
  },
  "createdAt": "2026-04-10T08:30:00+00:00"
}
```

#### Key fields

| Field                                                     | Description                                                                                                                  |
| --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `eventUuid`                                               | Unique event identifier in `zg-XXX` format. Use this for storage and correlation.                                            |
| `inviteeUuid`                                             | Unique invitee identifier in `zg-XXX` format. Use this to track a specific booking across schedule/cancel/reschedule events. |
| `hosts`                                                   | Object mapping host positions to names, e.g. `{"1": "Lena Meier"}`.                                                          |
| `hostsDetails`                                            | Array of host objects with `slug`, `firstName`, `lastName`, `email`, `fullName`.                                             |
| `inviteeStartAt`                                          | The event start time in the **invitee's timezone**. Useful for display in confirmation emails or UIs.                        |
| `cancelled` / `cancelledAt` / `cancelledBy`               | Populated when the event is `invitee.cancelled`. `cancelledBy` indicates who initiated the cancellation.                     |
| `rescheduled` / `rescheduledAt` / `rescheduleReason`      | Populated when a booking is rescheduled.                                                                                     |
| `oldEventUuid` / `newEventUuid`                           | When rescheduled, links the old and new events together.                                                                     |
| `price` / `currency` / `paymentGateway` / `transactionId` | Populated when the booking involves a paid event.                                                                            |
| `guests`                                                  | Array of additional guest email addresses added by the invitee.                                                              |

<Tip>
  Store `inviteeUuid` (and `eventUuid`) on your side. Invitee-related UUIDs are the best identifiers for correlating bookings, cancellations, and reschedules across webhook deliveries, and they ensure future compatibility -- especially for Group events.
</Tip>

### routing\_form.submitted

```json theme={null}
{
  "event": "routing_form.submitted",
  "reportId": "71621fb7-30b0-4d91-9f2e-a3e009cc6853",
  "routingFormId": "f1cbafc4-b646-4cf9-af29-193491b555d9",
  "routingFormName": "Inbound Lead Qualification",
  "routingFormSlug": "inbound-lead-qual",
  "routingFormUrl": "https://zeeg.me/RF/inbound-lead-qual",
  "routeType": "EVENT_TYPE",
  "routeCustomUrl": null,
  "headline": null,
  "isFallbackRoute": false,
  "eventType": {
    "id": "80f46bf5-eb01-4c07-960e-a9a3e18aae5e",
    "title": "30-Minute Discovery Call",
    "slug": "30min-discovery-call",
    "uri": "https://api.zeeg.me/v2/event-types/80f46bf5-eb01-4c07-960e-a9a3e18aae5e"
  },
  "answers": [
    {
      "question": "What is your company size?",
      "answer": "50-200 employees",
      "inputId": "f7c82298-6dd9-43bd-98e4-c437b5c0ae47",
      "order": 1
    }
  ],
  "questionsAndAnswers": {
    "What is your company size?": "50-200 employees"
  },
  "utmCampaign": "spring_launch",
  "utmSource": "linkedin",
  "utmMedium": null,
  "utmTerm": null,
  "utmContent": null,
  "adAttribution": {
    "gclid": "Cj0KCQjw_ndBhCrARIsAAy",
    "fbclid": "IwAR2xq9Zt",
    "landingUrl": "https://zeeg.me/RF/company-size?gclid=Cj0KCQjw_ndBhCrARIsAAy"
  },
  "createdAt": "2026-04-10T08:30:00+00:00"
}
```

#### Key fields

| Field                             | Description                                                                                                                       |
| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `routingFormId`                   | UUID of the routing form.                                                                                                         |
| `reportId`                        | Unique identifier for this specific submission.                                                                                   |
| `routeType`                       | How the visitor was routed (e.g., `EVENT_TYPE`).                                                                                  |
| `isFallbackRoute`                 | `true` if the default/fallback route was used.                                                                                    |
| `eventType`                       | The event type the visitor was routed to (with `id`, `title`, `slug`, `uri`).                                                     |
| `answers` / `questionsAndAnswers` | The visitor's responses. `answers` is an ordered array with `inputId` and `order`; `questionsAndAnswers` is a flat key-value map. |

### ai\_agent.call\_completed

<Badge color="orange">Alpha</Badge>

Fired once when an AI agent call reaches a terminal state (answered, voicemail, not answered, busy, or unknown). Contains the call outcome, transcript summary, any data the agent collected from the caller, actions the agent executed, and the booking created during the call (if any).

```json theme={null}
{
  "event": "ai_agent.call_completed",
  "conversationId": "abc1234-0000-0000-0000-xyz987654321",
  "agentId": "9a39bf60-a6c3-45e7-80cd-2cd36e520861",
  "agentName": "Sales Agent",
  "direction": "inbound",
  "callOutcome": "answered",
  "isCall": true,
  "startedAt": "2026-06-01T10:00:00+00:00",
  "endedAt": "2026-06-01T10:01:30+00:00",
  "durationSeconds": 90,
  "contactPhoneNumber": "+4917612345678",
  "agentPhoneNumber": "+4989123456789",
  "transcript": [
    { "role": "agent", "message": "Hello, thank you for calling. How can I help you today?", "timeInCallSeconds": 0 },
    { "role": "user", "message": "Hi, I'd like to know more about your pricing.", "timeInCallSeconds": 4 },
    { "role": "agent", "message": "Of course. Our plans start at 12 euros per month. Shall I book a follow-up call?", "timeInCallSeconds": 9 }
  ],
  "conversationAnalysis": {
    "transcriptSummary": "The caller asked about product pricing and requested a follow-up call next week.",
    "collectedData": {
      "name":    { "value": "Alice Müller", "type": "string" },
      "email":   { "value": "alice@example.com", "type": "string" },
      "message": { "value": "Please send me the pricing sheet.", "type": "string" },
      "summary": { "value": "Caller asked about pricing and wants a follow-up.", "type": "string" }
    }
  },
  "actions": [
    {
      "type": "book_event",
      "routeId": null,
      "calledAt": "2026-06-01T10:00:45+00:00"
    },
    {
      "type": "send_email",
      "routeId": "9b1c2d3e-4f56-7890-abcd-ef0123456789",
      "calledAt": "2026-06-01T10:01:20+00:00"
    }
  ],
  "booking": {
    "uri": "https://api.zeeg.me/v2/scheduled-events/zg-O69bd55e004c8b",
    "uuid": "zg-O69bd55e004c8b",
    "title": "30-Minute Meeting with Sales Agent",
    "type": "ONE_ON_ONE",
    "startTime": "2026-06-03T14:00:00.000000Z",
    "endTime": "2026-06-03T14:30:00.000000Z",
    "duration": 30,
    "status": "confirmed",
    "eventTypeUri": "https://api.zeeg.me/v2/event-types/80f46bf5-eb01-4c07-960e-a9a3e18aae5e",
    "location": { "type": "google_meet", "joinUrl": "https://meet.google.com/abc-defg-hij" },
    "maxActiveInvitees": 1,
    "activeInviteesCount": 1,
    "invitees": [
      {
        "uuid": "zg-A12cd34ef005a9c",
        "salutation": null,
        "fullName": "Alice Müller",
        "email": "alice@example.com",
        "guests": [],
        "timeZone": "Europe/Berlin",
        "cancellation": { "cancelledAt": null, "cancelledBy": null, "cancellerType": null, "cancellationReason": null },
        "payment": null,
        "questions": [],
        "scheduledAt": "2026-06-01T10:00:50.000000Z",
        "utm": [],
        "customQueryParams": [],
        "agentBookingReference": "123456"
      }
    ],
    "guests": [],
    "hosts": [
      { "firstName": "Lena", "lastName": "Meier", "email": "lena.meier@example.com", "slug": "lena-meier", "url": "https://zeeg.me/lena-meier", "avatarUrl": null }
    ],
    "createdAt": "2026-06-01T10:00:50.000000Z",
    "updatedAt": "2026-06-01T10:00:50.000000Z",
    "currentTime": "2026-06-01T10:01:30.000000Z"
  },
  "crmPersonId": "7f3e2a91-1234-4abc-9012-b3c456d78901"
}
```

#### Key fields

| Field                  | Description                                                                                                                                                                                                                                                                    |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `conversationId`       | Unique call identifier. Use this as the dedup key — in rare cases the same call may be delivered more than once.                                                                                                                                                               |
| `callOutcome`          | Terminal outcome: `answered`, `voicemail`, `not_answered`, `busy`, or `unknown`.                                                                                                                                                                                               |
| `direction`            | `inbound` (caller reached the agent) or `outbound` (agent dialled out).                                                                                                                                                                                                        |
| `transcript`           | Ordered conversation turns, each with `role`, `message`, and `timeInCallSeconds`. Tool-only turns with no spoken message are omitted.                                                                                                                                          |
| `conversationAnalysis` | AI-generated analysis of the conversation. Contains `transcriptSummary` (narrative summary) and `collectedData` (a map of collected field id → `{ value, type }`, e.g. `name`, `email`, `message`, `summary`, plus any custom fields).                                         |
| `actions`              | Ordered list of actions the agent executed. Each entry has a `type`, an optional `routeId` (the route that triggered it), and a `calledAt` timestamp.                                                                                                                          |
| `booking`              | The scheduled-event resource created during the call (same shape as `GET /v2/scheduled-events/{uuid}`), or `null` if no booking was made. Each entry in `invitees` carries `agentBookingReference` — the 6-digit reference for AI-agent bookings (`null` for non-AI bookings). |
| `crmPersonId`          | UUID of the CRM person record matched to this call, if any.                                                                                                                                                                                                                    |

<Tip>
  Use `conversationId` to deduplicate deliveries and `booking.uuid` to correlate with `invitee.scheduled` webhook events for the same booking.
</Tip>

## Managing webhooks

| Operation           | Method   | Endpoint                  |
| ------------------- | -------- | ------------------------- |
| List webhooks       | `GET`    | `/webhooks/scope/{scope}` |
| Get webhook details | `GET`    | `/webhooks/{uuid}`        |
| Delete webhook      | `DELETE` | `/webhooks/{uuid}`        |

Replace `{scope}` with `user` or `organization`.

You can also manage webhooks from the Zeeg dashboard:

**[Account Settings > Webhooks](https://app.zeeg.me/account/settings/webhooks)**

## Auto-deletion

<Warning>
  Zeeg automatically deletes a webhook subscription if any of the following conditions are met:

  * **DNS cannot be resolved** for the callback URL.
  * The callback URL returns a **404 Not Found** response.
  * The callback URL returns a **410 Gone** response.

  When a webhook is auto-deleted, the subscription owner is notified by email. Monitor your inbox to avoid missing events silently.
</Warning>

## Best practices

1. **Respond quickly.** Return a `2xx` status code as fast as possible. Zeeg expects a timely response from your callback URL.
2. **Process asynchronously.** Queue the payload for background processing rather than doing heavy work inside the request handler.
3. **Use the verification token.** Always set a `token` when creating a webhook and validate it on every incoming request.
4. **Handle duplicates idempotently.** In rare cases, Zeeg may deliver the same event more than once. Use `eventUuid` or `inviteeUuid` to deduplicate.
5. **Monitor for auto-deletion emails.** If your endpoint goes down and Zeeg deletes the subscription, you will only know through the notification email. Set up alerting on your side.
