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

# Webhooks

> Receive real-time notifications when interview sessions are processed

Webhooks allow you to receive automatic notifications when events occur in your Hallway account. When an interview session is processed, Hallway sends the transcript and insights directly to your endpoint—enabling integrations with Slack, Zapier, custom AI analysis tools, or your own systems.

## What You Can Build

With webhooks, you can:

* **Slack notifications**: Get alerts when new interviews are completed
* **Zapier automation**: Trigger workflows when transcripts are ready
* **Custom analysis**: Send transcripts to your own AI models
* **CRM integration**: Update customer records with interview data
* **Real-time dashboards**: Push data to analytics platforms

## Requirements

<Note>
  Webhooks are available exclusively on the **Pro plan**. Free users can upgrade in Settings → Billing.
</Note>

Before setting up webhooks:

* A Hallway Pro subscription
* An endpoint URL that can receive POST requests (HTTPS required)
* Basic understanding of webhook security (HMAC verification)

## Creating a Webhook

<Steps>
  <Step title="Open Integrations Settings">
    Go to **Settings** → **Integrations** tab. Scroll down to the **Webhooks** section.
  </Step>

  <Step title="Click Add Webhook">
    Click the **Add Webhook** button. A dialog will open for configuration.
  </Step>

  <Step title="Configure Webhook">
    Fill in the required fields:

    * **Name**: A friendly name (e.g., "Slack Notifications")
    * **URL**: Your HTTPS endpoint that will receive webhook payloads
    * **Description** (optional): Notes about what this webhook does
  </Step>

  <Step title="Select Events">
    Choose which events trigger this webhook. Currently available:

    * `session.processing.completed` - Triggered when an interview is fully processed
  </Step>

  <Step title="Filter by Project (Optional)">
    By default, webhooks fire for all projects. To limit to specific projects, select them from the dropdown.
  </Step>

  <Step title="Save and Copy Secret">
    Click **Create Webhook**. A signing secret will be displayed.

    <Warning>
      Copy and save this secret immediately. It will only be shown once. You need it to verify webhook signatures.
    </Warning>
  </Step>
</Steps>

## Webhook Payload

When an event occurs, Hallway sends a POST request to your endpoint with a JSON payload.

### Headers

| Header                | Description                                            |
| --------------------- | ------------------------------------------------------ |
| `Content-Type`        | `application/json`                                     |
| `X-Hallway-Signature` | HMAC signature for verification (see Security section) |
| `User-Agent`          | `Hallway-Webhooks/1.0`                                 |

### Payload Structure

```json theme={null}
{
  "id": "evt_m5k2j_8x7yAbCdEf",
  "type": "session.processing.completed",
  "created_at": "2026-01-10T15:30:00.000Z",
  "api_version": "2026-01-01",
  "account": {
    "id": "acc_abc123",
    "name": "Acme Research"
  },
  "project": {
    "id": "prj_xyz789",
    "name": "Customer Discovery Q1",
    "status": "active"
  },
  "session": {
    "id": "ses_def456",
    "respondent_name": "Jane Doe",
    "respondent_email": "jane@example.com",
    "duration_seconds": 342,
    "completed_at": "2026-01-10T15:25:00.000Z"
  },
  "respondent_attributes": [
    { "category_name": "Department", "option_value": "Engineering" },
    { "category_name": "Role", "option_value": "Manager" }
  ],
  "transcript": {
    "id": "trx_ghi789",
    "full_text": "Interviewer: Hello and welcome...\n\nRespondent: Thank you for having me...",
    "word_count": 1240
  },
  "insights": {
    "count": 8,
    "objectives_covered": 3
  },
  "links": {
    "session": "https://app.hallwayai.com/projects/prj_xyz789/sessions/ses_def456",
    "project": "https://app.hallwayai.com/projects/prj_xyz789"
  }
}
```

### Field Descriptions

| Field                   | Type   | Description                                   |
| ----------------------- | ------ | --------------------------------------------- |
| `id`                    | string | Unique event ID for idempotency               |
| `type`                  | string | Event type that triggered the webhook         |
| `created_at`            | string | ISO 8601 timestamp of when the event occurred |
| `api_version`           | string | API version for payload schema                |
| `account`               | object | Your Hallway account information              |
| `project`               | object | Project the session belongs to                |
| `session`               | object | Interview session details                     |
| `respondent_attributes` | array  | Custom attributes collected from respondent   |
| `transcript`            | object | Full transcript text and metadata             |
| `insights`              | object | Summary of extracted insights                 |
| `links`                 | object | Direct URLs to view in Hallway                |

## Security

### Signature Verification

Every webhook includes an `X-Hallway-Signature` header for verification. **Always verify signatures** to ensure requests come from Hallway.

The signature format is:

```
t={timestamp},v1={signature}
```

Where:

* `t` is the Unix timestamp (seconds) when the webhook was sent
* `v1` is the HMAC-SHA256 signature

### Verification Steps

<Steps>
  <Step title="Extract Components">
    Parse the `X-Hallway-Signature` header to get the timestamp and signature.
  </Step>

  <Step title="Prepare Signed Payload">
    Concatenate the timestamp and raw request body with a period:

    ```
    {timestamp}.{raw_body}
    ```
  </Step>

  <Step title="Compute Expected Signature">
    Calculate HMAC-SHA256 of the signed payload using your webhook secret.
  </Step>

  <Step title="Compare Signatures">
    Compare your computed signature with the one in the header using constant-time comparison.
  </Step>

  <Step title="Check Timestamp">
    Verify the timestamp is within 5 minutes of current time to prevent replay attacks.
  </Step>
</Steps>

### Code Examples

<Tabs>
  <Tab title="Node.js">
    ```javascript theme={null}
    const crypto = require('crypto');

    function verifyWebhook(payload, signature, secret) {
      // Parse signature header
      const parts = signature.split(',');
      const timestamp = parts.find(p => p.startsWith('t='))?.split('=')[1];
      const sig = parts.find(p => p.startsWith('v1='))?.split('=')[1];

      if (!timestamp || !sig) {
        throw new Error('Invalid signature format');
      }

      // Check timestamp (5 minute tolerance)
      const now = Math.floor(Date.now() / 1000);
      if (Math.abs(now - parseInt(timestamp)) > 300) {
        throw new Error('Timestamp too old');
      }

      // Compute expected signature
      const signedPayload = `${timestamp}.${payload}`;
      const expected = crypto
        .createHmac('sha256', secret)
        .update(signedPayload)
        .digest('hex');

      // Constant-time comparison
      if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
        throw new Error('Signature mismatch');
      }

      return true;
    }
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import hmac
    import hashlib
    import time

    def verify_webhook(payload: str, signature: str, secret: str) -> bool:
        # Parse signature header
        parts = dict(p.split('=') for p in signature.split(','))
        timestamp = parts.get('t')
        sig = parts.get('v1')

        if not timestamp or not sig:
            raise ValueError('Invalid signature format')

        # Check timestamp (5 minute tolerance)
        now = int(time.time())
        if abs(now - int(timestamp)) > 300:
            raise ValueError('Timestamp too old')

        # Compute expected signature
        signed_payload = f"{timestamp}.{payload}"
        expected = hmac.new(
            secret.encode(),
            signed_payload.encode(),
            hashlib.sha256
        ).hexdigest()

        # Constant-time comparison
        if not hmac.compare_digest(sig, expected):
            raise ValueError('Signature mismatch')

        return True
    ```
  </Tab>

  <Tab title="Ruby">
    ```ruby theme={null}
    require 'openssl'

    def verify_webhook(payload, signature, secret)
      # Parse signature header
      parts = signature.split(',').map { |p| p.split('=') }.to_h
      timestamp = parts['t']
      sig = parts['v1']

      raise 'Invalid signature format' unless timestamp && sig

      # Check timestamp (5 minute tolerance)
      now = Time.now.to_i
      raise 'Timestamp too old' if (now - timestamp.to_i).abs > 300

      # Compute expected signature
      signed_payload = "#{timestamp}.#{payload}"
      expected = OpenSSL::HMAC.hexdigest('sha256', secret, signed_payload)

      # Constant-time comparison
      raise 'Signature mismatch' unless secure_compare(sig, expected)

      true
    end

    def secure_compare(a, b)
      return false unless a.bytesize == b.bytesize
      a.bytes.zip(b.bytes).reduce(0) { |acc, (x, y)| acc | (x ^ y) }.zero?
    end
    ```
  </Tab>
</Tabs>

## Managing Webhooks

### Testing Webhooks

To verify your endpoint is working:

1. Find your webhook in **Settings** → **Integrations** → **Webhooks**
2. Click the **more options** menu (three dots)
3. Select **Send Test**
4. A sample payload will be sent to your endpoint

Test payloads include `"_test": true` so you can identify them.

### Viewing Delivery History

To see webhook delivery attempts:

1. Click the **more options** menu for your webhook
2. Select **Delivery History**
3. View recent deliveries with status, response time, and response codes

### Rotating Secrets

If you need to rotate your webhook secret:

1. Click the **more options** menu
2. Select **Rotate Secret**
3. A new secret will be generated
4. Update your endpoint with the new secret immediately

<Warning>
  After rotating, the old secret is immediately invalidated. Update your endpoint before rotating to avoid missing webhooks.
</Warning>

### Disabling and Deleting

* **Disable**: Toggle the webhook off to pause deliveries without deleting configuration
* **Delete**: Remove the webhook permanently via the options menu

## Retry Logic

If your endpoint fails to respond, Hallway retries with exponential backoff:

| Attempt | Delay      | Total Wait  |
| ------- | ---------- | ----------- |
| 1       | Immediate  | 0           |
| 2       | 1 minute   | 1 minute    |
| 3       | 5 minutes  | 6 minutes   |
| 4       | 30 minutes | 36 minutes  |
| 5       | 2 hours    | \~2.5 hours |

### Success Criteria

A delivery is considered successful when your endpoint returns:

* HTTP status code 2xx (200, 201, 202, etc.)
* Response within 30 seconds

### Auto-Disable

If a webhook fails **10 consecutive times**, it will be automatically disabled to prevent further failures. You'll need to manually re-enable it after fixing the issue.

## Limits

| Limit                         | Value      |
| ----------------------------- | ---------- |
| Maximum webhooks per account  | 5          |
| Request timeout               | 30 seconds |
| Maximum retry attempts        | 5          |
| Signature timestamp tolerance | 5 minutes  |

## Troubleshooting

### Webhook Not Triggering

* Verify the webhook is **enabled** (not toggled off)
* Check that the event type matches (e.g., `session.processing.completed`)
* Ensure the project is included if you set project filters
* Review delivery history for errors

### Signature Verification Failing

* Ensure you're using the **raw request body** (not parsed JSON)
* Check that your secret is correct (no extra whitespace)
* Verify the timestamp is within 5 minutes
* Use constant-time string comparison

### Endpoint Returning Errors

* Your endpoint must return a 2xx status code
* Response must complete within 30 seconds
* Check your server logs for errors
* Use the **Send Test** feature to debug

### Webhook Auto-Disabled

* Check delivery history for the failure pattern
* Fix the underlying issue (endpoint down, auth errors, etc.)
* Re-enable the webhook in Settings
* Consider using the test feature before re-enabling

## Best Practices

<CardGroup cols={2}>
  <Card title="Verify signatures" icon="shield-check">
    Always verify the X-Hallway-Signature header before processing payloads.
  </Card>

  <Card title="Respond quickly" icon="bolt">
    Return a 2xx response immediately, then process asynchronously.
  </Card>

  <Card title="Handle duplicates" icon="clone">
    Use the event `id` field for idempotency in case of retries.
  </Card>

  <Card title="Monitor delivery history" icon="chart-line">
    Regularly check delivery history to catch issues early.
  </Card>
</CardGroup>

## FAQ

<AccordionGroup>
  <Accordion title="Are webhooks included with Pro?">
    Yes, webhooks are included with your Pro subscription at no additional cost. You can create up to 5 webhooks per account.
  </Accordion>

  <Accordion title="Can I filter webhooks by project?">
    Yes. When creating or editing a webhook, you can select specific projects. Leave empty to receive events from all projects.
  </Accordion>

  <Accordion title="What if my endpoint is down?">
    Hallway retries failed deliveries up to 5 times with exponential backoff over approximately 2.5 hours. After 10 consecutive failures, the webhook is auto-disabled.
  </Accordion>

  <Accordion title="How do I test without processing a real interview?">
    Use the **Send Test** feature in the webhook options menu. This sends a sample payload with realistic data and `"_test": true` flag.
  </Accordion>

  <Accordion title="Can I receive webhooks for failed sessions?">
    Currently, webhooks only fire for successfully processed sessions (`session.processing.completed`). Failed session events may be added in the future.
  </Accordion>

  <Accordion title="Is there a way to see the actual payload sent?">
    Delivery history shows status and timing but not the full payload. For debugging, log incoming requests on your endpoint.
  </Accordion>

  <Accordion title="What IP addresses do webhooks come from?">
    Webhooks are sent from Vercel Edge Functions. Due to dynamic IP allocation, we recommend verifying signatures rather than IP allowlisting.
  </Accordion>
</AccordionGroup>

## Next Steps

* [Claude AI Integration](/docs/help/integrations/claude-ai) - Connect Claude to analyze your research data
* [Understanding Insights](/docs/help/insights/understanding-insights) - Learn how insights are extracted
* [Exporting Data](/docs/help/insights/exporting-data) - Other ways to export your data
