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

# Custom webhooks

> Programmatic HTTP integration for triggering calls and receiving results.

Custom webhooks give developers full control over the hyzl integration layer. You can trigger calls with an HTTP POST. You can also receive call results at any endpoint you specify.

hyzl carries them as two cards on the Integrations page. They are **Custom inbound webhook** and **Custom outbound webhook**.

<Note>
  Custom webhooks are intended for developers and technical users. If you would
  prefer a no-code solution, use [Zapier
  Inbound](/docs/integrations/zapier-inbound) to trigger calls and [Zapier
  Outbound](/docs/integrations/zapier-outbound) to receive results instead.
</Note>

<Note>
  Webhooks require an active AI Calling subscription. If your subscription
  lapses, hyzl pauses your existing webhooks automatically. They resume when you
  resubscribe. If you create a new webhook without an active subscription, the
  request returns HTTP 402.
</Note>

## Inbound webhooks: trigger a call

Inbound webhooks let external systems trigger AI calls on-demand. When your customer relationship manager (CRM) sends a request to your inbound webhook URL, hyzl starts a call automatically. Any other system can send the same request.

### Set up an inbound webhook

1. Go to **Integrations**
2. Find the **Custom inbound webhook** card and click **View integration**
3. Click **Create inbound webhook**
4. Click **Create Campaign** to set up the AI script
5. Write your prompt
6. Test the prompt
7. Use the webhook URL to trigger calls from your CRM

The **+ Custom integration** menu in the page header runs the same flow, under **Custom inbound webhook**.

hyzl creates the webhook with a unique slug as soon as you click **Create inbound webhook**.

### Endpoint

```
POST https://wh.atll.as/{your-webhook-slug}
```

hyzl binds each inbound webhook to one campaign when you create it. You do not need to name the campaign in each request.

### Request body

```json theme={null}
{
  "type": "calls.execute",
  "data": {
    "phoneNumber": "+15551234567",
    "firstName": "Jane",
    "lastName": "Smith",
    "email": "jane@example.com",
    "companyName": "Acme Corp",
    "note": "Expressed interest in the enterprise plan"
  }
}
```

| Field                                                                       | Required | Description                                   |
| --------------------------------------------------------------------------- | -------- | --------------------------------------------- |
| `type`                                                                      | Yes      | Must be `"calls.execute"`                     |
| `data.phoneNumber`                                                          | Yes      | E.164 format recommended (`+15551234567`)     |
| `data.firstName`                                                            | No       | Used for personalization                      |
| `data.lastName`                                                             | No       |                                               |
| `data.email`                                                                | No       | Used for email follow-ups if configured       |
| `data.companyName`                                                          | No       |                                               |
| `data.note`                                                                 | No       | Additional context for this contact           |
| `data.address1`, `data.address2`, `data.city`, `data.state`, `data.zipCode` | No       | Address fields, available for personalization |

<Note>
  Field names are camelCase. hyzl ignores snake\_case names such as
  `first_name` and `phone_number`. It places the call with `phoneNumber` only.
</Note>

### Response

```json theme={null}
{ "success": true }
```

A `201` response means hyzl accepted the request. The campaign attached to the webhook must be active before hyzl places the call. If you are out of credits, hyzl records the call as cancelled and still returns the same response status.

### Customize the phone call

The webhook is bound to an **inbound campaign** in AI Calling. That campaign owns every call setting, including voice, call script, voicemail message, and follow-ups. It also owns call forwarding, keypad navigation, language, and background ambiance. Whatever is on the campaign is what every webhook-triggered call uses.

To change call behavior:

1. Open **AI Calling**
2. Find the campaign your webhook uses. hyzl labels it as inbound.
3. Edit any setting: voice, script, follow-ups, voicemail, etc.
4. Save

Inbound campaigns can be edited at any time (unlike outbound campaigns, which lock once they start dialing). Changes apply to all future calls triggered by the webhook. Already-completed calls keep the settings they ran with.

### View results

Every call triggered by the webhook appears in the bound campaign's results table in **AI Calling**. That is the same place outbound campaign results live.

Each call result includes the transcript, recording, AI summary, lead warmth classification, and extracted action items. See [Call Results & Transcripts](/docs/ai-calling/managing-campaigns/call-results-and-transcripts) for what is in the detail panel.

You can also get each completed call at your own endpoint as it happens. To do that, configure a [Response Webhook](#response-webhooks) on the inbound webhook. The section below covers it. hyzl POSTs the result back to your URL when the call ends.

***

## Outbound webhooks: receive call results

Outbound webhooks broadcast **every call completion event** to your configured endpoint(s). Whenever a call finishes, hyzl POSTs the results to every outbound webhook URL you have configured. The call can come from a manual campaign, an inbound webhook trigger, or a test call.

<Note>
  Creating an outbound webhook manually is only for custom webhooks. If you want
  to send call results to Zapier, set up an [outbound Zapier
  integration](/docs/integrations/zapier-outbound) instead. hyzl creates its
  outbound webhook automatically and labels it **Managed by zapier**.
</Note>

### Set up an outbound webhook

1. Go to **Integrations**
2. Find the **Custom outbound webhook** card and click **View integration**
3. Click **Create outbound webhook**
4. Enter your endpoint URL
5. Choose the HTTP method (POST or GET)
6. Add custom headers for authentication if you need them (see [Custom Headers](#custom-headers))
7. Click **Create Webhook** to save it
8. Click the **Test** button on the webhook's card to check connectivity

The **+ Custom integration** menu in the page header runs the same flow, under **Custom outbound webhook**.

### Event

Outbound webhooks emit a single event:

| Event           | When                                                                                             |
| --------------- | ------------------------------------------------------------------------------------------------ |
| `call.finished` | Fires after the call completes and final analysis is attached (transcript, summary, lead warmth) |

### Payload

```json theme={null}
{
  "type": "call.finished",
  "timestamp": 1730179200000,
  "webhook": {
    "id": "webhook_abc123",
    "type": "outbound"
  },
  "call": {
    "id": "call_xyz789",
    "status": "finished",
    "recipient": {
      "phoneNumber": "+15551234567",
      "firstName": "Jane",
      "lastName": "Smith",
      "email": "jane@example.com",
      "address1": "123 Main St",
      "city": "Austin",
      "state": "TX",
      "zipCode": "78701",
      "note": "VIP lead"
    },
    "summary": "Jane expressed interest in the enterprise plan and asked about pricing.",
    "transcript": [
      {
        "who": "assistant",
        "what": "Hi, is this Jane?",
        "when": "2024-04-30T14:00:03.000Z"
      },
      {
        "who": "user",
        "what": "Yes, speaking.",
        "when": "2024-04-30T14:00:05.000Z"
      }
    ],
    "recording": "https://...",
    "duration": 142000,
    "analysis": {
      "sentiment": "positive",
      "lead_warmth": "hot",
      "justification": "Expressed immediate interest and asked specific pricing questions."
    },
    "tokensConsumed": 1
  },
  "campaign": {
    "id": "campaign_123",
    "name": "Q4 Outreach"
  }
}
```

### Field reference

| Path                                                                           | Description                                                                                                                                                 |
| ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`                                                                         | Always `"call.finished"`                                                                                                                                    |
| `timestamp`                                                                    | Unix epoch in milliseconds                                                                                                                                  |
| `webhook.id`                                                                   | The outbound webhook's ID                                                                                                                                   |
| `webhook.type`                                                                 | Always `"outbound"`                                                                                                                                         |
| `call.id`                                                                      | The call ID                                                                                                                                                 |
| `call.status`                                                                  | One of: `finished`, `missed`, `failed`                                                                                                                      |
| `call.recipient.phoneNumber`                                                   | Contact phone in E.164                                                                                                                                      |
| `call.recipient.firstName` / `lastName` / `email`                              | Contact identity fields                                                                                                                                     |
| `call.recipient.address1` / `address2` / `city` / `state` / `zipCode` / `note` | Optional address + note                                                                                                                                     |
| `call.summary`                                                                 | One-paragraph AI summary                                                                                                                                    |
| `call.transcript`                                                              | Array of conversation turns. `who` is typically `"user"` (caller) or `"assistant"` (AI), and `when` is an ISO timestamp. Each turn is `{ who, what, when }` |
| `call.recording`                                                               | URL to the MP3 recording                                                                                                                                    |
| `call.duration`                                                                | Call length in **milliseconds**                                                                                                                             |
| `call.analysis.sentiment`                                                      | `positive`, `neutral`, or `negative`                                                                                                                        |
| `call.analysis.lead_warmth`                                                    | `hot`, `warm`, `cold`, `dead`, or `unknown`                                                                                                                 |
| `call.analysis.justification`                                                  | AI's reasoning for the warmth classification                                                                                                                |
| `call.tokensConsumed`                                                          | Credits used for the call                                                                                                                                   |
| `campaign.id` / `campaign.name`                                                | Source campaign info                                                                                                                                        |

### Status values

| Status     | Meaning                                         |
| ---------- | ----------------------------------------------- |
| `finished` | Call completed successfully                     |
| `missed`   | The contact did not answer or went to voicemail |
| `failed`   | Technical error occurred                        |

### Lead Warmth values

| Value     | Meaning                              |
| --------- | ------------------------------------ |
| `hot`     | High interest, ready to move forward |
| `warm`    | Some interest, needs nurturing       |
| `cold`    | Low interest or not qualified        |
| `dead`    | No interest or wrong number          |
| `unknown` | Unable to determine                  |

### Sentiment values

| Value      | Meaning                           |
| ---------- | --------------------------------- |
| `positive` | Friendly, receptive conversation  |
| `neutral`  | Professional, factual interaction |
| `negative` | Frustrated or hostile             |

<Note>
  The `call.recipient` field names are camelCase (`firstName`, `phoneNumber`).
  It has no `company` field. Build your integrations against the names shown
  above.
</Note>

***

## Response webhooks

You configure a response webhook on an inbound webhook. It sends the call result back to one endpoint after an inbound-triggered call completes. This forms a request/response pattern.

### Set up a response webhook

1. Click **Webhook Settings** on your inbound webhook
2. Scroll to **Response Webhook (Optional)**
3. Enter your callback URL
4. Add custom headers if you need them

The response webhook fires with `type: "call.completed"`. Its payload has the same overall shape as the outbound webhook payload, with two differences.

First, `recipient`, `summary`, `transcript`, `recording`, `duration`, `analysis`, and `tokensConsumed` are all optional. Some calls never connect, so they will not have all fields populated. Second, the `webhook` object carries the inbound webhook's `slug` instead of `type` (`{ "id": "...", "slug": "your-webhook-slug" }`).

Outbound webhooks use `type: "call.finished"`, not `call.completed`.

***

## Custom Headers

Custom headers let you authenticate against CRMs or other systems that require API keys or access tokens.

### Add headers

1. Expand **Custom Headers (Optional)** while you create or edit a webhook
2. Click **Add Header**
3. Enter header name and value (e.g. `Authorization`, `Bearer token123`)

hyzl encrypts your headers at rest. The portal never shows them again after you save.

### FollowUp Boss example

FollowUp Boss requires these headers:

```
X-System: your-app-name
X-System-Key: your-api-key
Authorization: Basic base64(api-key:)
```

Add them as:

* `X-System` → `atllas-integration`
* `X-System-Key` → `your-followupboss-api-key`
* `Authorization` → `Basic {base64-encoded-key}`

### HubSpot example

HubSpot workflow webhooks typically do not require custom headers. Just configure the webhook URL in your HubSpot workflow.

For HubSpot API endpoints, add:

```
Authorization: Bearer your-access-token
```

***

## Tests and logs

### Test button

Each webhook has a **Test** button that sends a `call.completed`-shaped payload with example data:

```json theme={null}
{
  "type": "call.completed",
  "timestamp": 1730179200000,
  "webhook": { "id": "webhook_test_outbound_456", "type": "outbound" },
  "call": {
    "id": "call_test_abc123def456",
    "status": "finished",
    "recipient": {
      "phoneNumber": "+15555551234",
      "firstName": "Jessica",
      "lastName": "Smith",
      "email": "jessica.smith@example.com"
    },
    "summary": "Example call summary from the test button.",
    "transcript": [
      {
        "who": "assistant",
        "what": "Hi Jessica, this is Alex.",
        "when": "2024-04-30T14:00:03.000Z"
      },
      {
        "who": "user",
        "what": "I'm doing well, thanks for calling!",
        "when": "2024-04-30T14:00:05.000Z"
      }
    ],
    "recording": "https://storage.googleapis.com/atllas-call-recordings/test-recording-example.mp3",
    "duration": 123456,
    "analysis": {
      "sentiment": "positive",
      "lead_warmth": "hot",
      "justification": "Example justification."
    },
    "tokensConsumed": 42
  },
  "campaign": { "id": "campaign_test_xyz789", "name": "Example Campaign" }
}
```

The test validates:

* The URL is reachable from our servers
* Your custom headers are correct
* Your endpoint responds with a 2xx status

<Warning>
  Handle both event types before you rely on this in production. The test payload
  uses `call.completed`, and a real outbound webhook delivery uses
  `call.finished`. This matters when your endpoint routes behavior by event
  type.
</Warning>

***

## Fix common problems

### Webhook not firing

**For inbound webhooks:**

* Check that you created the campaign (click **Create Campaign**)
* Check the campaign has a prompt configured
* Make sure you send the correct payload format (`calls.execute`)

**For outbound webhooks:**

* Check the URL is correct and reachable
* Use the **Test** button to check connectivity

### Authentication errors

If receiving 401 or 403 errors:

* Check custom headers are correct
* Check that the API key has not expired
* Test with a simple endpoint (such as `webhook.site`) first
* Review the destination system's authentication requirements

### Call not triggering from inbound webhook

* Make sure campaign status is not `executing` or `completed`
* Check you have available calling credits. hyzl records an out-of-credit call as **cancelled** in the campaign's results
* Check the phone number is in correct format (`+1...`)

***

## Security best practices

1. **Keep webhook URLs secret**. Treat them like API keys
2. **Use HTTPS only**. Never use HTTP endpoints
3. **Rotate headers periodically**. Update API keys regularly
4. **Test in staging first**. Use the **Test** button before production
