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

# Webhook Configuration

## Overview

HeyMilo Webhooks allow you to receive real-time HTTP notifications when key events occur during the candidate interview lifecycle. Instead of polling the API for updates, webhooks push data to your specified endpoint as soon as an event happens — enabling seamless automation, ATS syncing, and custom workflow triggers.

With webhooks, you can:

* Get notified instantly when a candidate starts or completes an interview
* Receive full interview reports as soon as they are generated
* Automate downstream actions like ATS updates, email notifications, or scoring pipelines
* Build event-driven integrations without continuous API polling

## How Webhooks Work

1. **Register a webhook** via the API, specifying a destination URL, the event type you want to subscribe to, and the posting (job) it applies to.
2. **HeyMilo fires an HTTP request** to your URL whenever the subscribed event occurs.
3. **Your endpoint receives the payload** containing all relevant event data (candidate info, interview details, scores, etc.).

```text theme={null}
┌──────────┐         Event occurs         ┌────────────────┐
│  HeyMilo │ ──── HTTP POST/GET ────────► │  Your Endpoint │
└──────────┘   (JSON payload)             └────────────────┘
```

***

## Authentication

Webhook API endpoints require authentication using the `X-API-KEY` header, the same key used for all Public API requests.

```http theme={null}
X-API-KEY: your_api_key_here
```

***

## Webhook Event Types

HeyMilo supports the following webhook event types:

| Event Type            | Description                                                       |
| --------------------- | ----------------------------------------------------------------- |
| `interview_started`   | Triggered when a candidate begins an interview session            |
| `interview_completed` | Triggered when a candidate finishes all steps of an interview     |
| `report_available`    | Triggered when the AI-generated interview report is ready to view |

### Event Lifecycle

A typical candidate journey triggers events in this order:

```text theme={null}
interview_started  ──►  interview_completed  ──►  report_available
```

> **Note:** The `report_available` event fires after AI analysis is complete, which may occur several seconds to minutes after the interview is marked as completed.

***

## Webhook API Endpoints

All webhook endpoints use the base URL:

```text theme={null}
https://api.heymilo.network/api/webhook
```

### 1. Create Webhook

**POST** `/create`

Register a new webhook to receive event notifications for a specific job posting.

#### Request Body

```json theme={null}
{
  "posting_id": "posting_123",
  "url": "https://your-domain.com/webhook-endpoint",
  "event_type": "interview_completed",
  "http_method": "post"
}
```

#### Request Schema

| Field         | Type         | Required | Description                                                                |
| ------------- | ------------ | -------- | -------------------------------------------------------------------------- |
| `posting_id`  | string       | Yes      | ID of the job posting this webhook applies to                              |
| `url`         | string (URL) | Yes      | The destination URL that will receive the webhook payload                  |
| `event_type`  | string       | Yes      | One of: `interview_started`, `interview_completed`, `report_available`     |
| `http_method` | string       | No       | HTTP method used to deliver the webhook: `get` or `post` (default: `post`) |

#### Example Request

```bash theme={null}
curl -X POST "https://api.heymilo.network/api/webhook/create" \
  -H "X-API-KEY: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "posting_id": "posting_123",
    "url": "https://your-domain.com/webhook-endpoint",
    "event_type": "interview_completed",
    "http_method": "post"
  }'
```

#### Response

```json theme={null}
{
  "webhook_id": "webhook_789",
  "posting_id": "posting_123",
  "url": "https://your-domain.com/webhook-endpoint",
  "event_type": "interview_completed",
  "http_method": "post",
  "is_active": true,
  "created_at": 1703097600.0,
  "updated_at": 1703097600.0
}
```

#### Response Schema

| Field         | Type    | Description                                       |
| ------------- | ------- | ------------------------------------------------- |
| `webhook_id`  | string  | Unique identifier for the created webhook         |
| `posting_id`  | string  | The job posting this webhook is associated with   |
| `url`         | string  | Destination URL for webhook delivery              |
| `event_type`  | string  | The subscribed event type                         |
| `http_method` | string  | HTTP method used for delivery (`get` or `post`)   |
| `is_active`   | boolean | Whether the webhook is currently active           |
| `created_at`  | float   | Unix timestamp when the webhook was created       |
| `updated_at`  | float   | Unix timestamp when the webhook was last modified |

***

### 2. Get Webhook

**GET** `/fetch/{webhook_id}`

Retrieve a specific webhook configuration by its ID.

#### Path Parameters

| Parameter    | Type   | Required | Description                  |
| ------------ | ------ | -------- | ---------------------------- |
| `webhook_id` | string | Yes      | The unique ID of the webhook |

#### Example Request

```bash theme={null}
curl -X GET "https://api.heymilo.network/api/webhook/fetch/webhook_789" \
  -H "X-API-KEY: your_api_key_here"
```

#### Response

```json theme={null}
{
  "webhook_id": "webhook_789",
  "posting_id": "posting_123",
  "url": "https://your-domain.com/webhook-endpoint",
  "event_type": "interview_completed",
  "http_method": "post",
  "is_active": true,
  "created_at": 1703097600.0,
  "updated_at": 1703097600.0
}
```

***

### 3. Get All Webhooks

**GET** `/all`

Retrieve all webhooks for your workspace, with optional filters.

#### Query Parameters

| Parameter    | Type   | Required | Description                                                                          |
| ------------ | ------ | -------- | ------------------------------------------------------------------------------------ |
| `posting_id` | string | No       | Filter webhooks by job posting ID                                                    |
| `event_type` | string | No       | Filter by event type: `interview_started`, `interview_completed`, `report_available` |

#### Example Request

```bash theme={null}
curl -X GET "https://api.heymilo.network/api/webhook/all?posting_id=posting_123&event_type=interview_completed" \
  -H "X-API-KEY: your_api_key_here"
```

#### Response

```json theme={null}
[
  {
    "webhook_id": "webhook_789",
    "posting_id": "posting_123",
    "url": "https://your-domain.com/webhook-endpoint",
    "event_type": "interview_completed",
    "http_method": "post",
    "is_active": true,
    "created_at": 1703097600.0,
    "updated_at": 1703097600.0
  },
  {
    "webhook_id": "webhook_790",
    "posting_id": "posting_123",
    "url": "https://your-domain.com/report-endpoint",
    "event_type": "report_available",
    "http_method": "post",
    "is_active": true,
    "created_at": 1703097700.0,
    "updated_at": 1703097700.0
  }
]
```

***

### 4. Deactivate Webhook

**POST** `/deactivate/{webhook_id}`

Deactivate an existing webhook. Deactivated webhooks will no longer receive event notifications.

#### Path Parameters

| Parameter    | Type   | Required | Description                  |
| ------------ | ------ | -------- | ---------------------------- |
| `webhook_id` | string | Yes      | The unique ID of the webhook |

#### Example Request

```bash theme={null}
curl -X POST "https://api.heymilo.network/api/webhook/deactivate/webhook_789" \
  -H "X-API-KEY: your_api_key_here"
```

***

## Webhook Payload Schemas

When an event fires, HeyMilo sends an HTTP request to your registered URL with a JSON payload. The payload structure varies by event type. Each event type has a distinct schema — see the full payloads and field-by-field breakdowns below.

***

### Payload: `interview_started`

Sent when a candidate begins an interview session.

```json theme={null}
{
  "interview_id": "A1B2C3D4E5F60718",
  "posting_id": "F9E8D7C6",
  "timestamp": 1770816760.310334,
  "metadata": {
    "candidate_name": "Jane Doe",
    "candidate_email": "jane.doe@example.com",
    "candidate_id": "B4C5D6E7",
    "metadata": {
      "source": "user_ingestion",
      "job_id": "F9E8D7C6"
    }
  }
}
```

#### `interview_started` Payload Schema

| Field          | Type   | Description                               |
| -------------- | ------ | ----------------------------------------- |
| `interview_id` | string | Unique identifier for the interview       |
| `posting_id`   | string | The job posting ID                        |
| `timestamp`    | float  | Unix timestamp when the interview started |
| `metadata`     | object | Candidate metadata object (see below)     |

**Candidate Metadata Object:**

| Field             | Type        | Description                                                              |
| ----------------- | ----------- | ------------------------------------------------------------------------ |
| `candidate_name`  | string      | Candidate's full name                                                    |
| `candidate_email` | string      | Candidate's email address                                                |
| `candidate_id`    | string      | Unique identifier for the candidate                                      |
| `metadata`        | object/null | Additional ingestion metadata (can be `null`). See inner metadata below. |

**Inner Metadata Object** *(nullable)*:

| Field    | Type   | Description                                             |
| -------- | ------ | ------------------------------------------------------- |
| `source` | string | How the candidate was ingested (e.g., `user_ingestion`) |
| `job_id` | string | The job/posting ID associated with the ingestion        |

> **Note:** The inner `metadata` object can be `null` if no additional ingestion context is available.

***

### Payload: `interview_completed`

Sent when a candidate finishes all required steps of an interview.

```json theme={null}
{
  "interview_id": "A1B2C3D4E5F60718",
  "agent_conversation_id": "",
  "parent_conversation_id": "7A8B9C0D1E2F3A4B",
  "engagement_id": "",
  "posting_id": "F9E8D7C6",
  "timestamp": 1770810397.0832531,
  "workflow_completed": true,
  "completion_timestamp": 1770810397.0827055
}
```

#### `interview_completed` Payload Schema

| Field                    | Type    | Description                                                                |
| ------------------------ | ------- | -------------------------------------------------------------------------- |
| `interview_id`           | string  | Unique identifier for the interview                                        |
| `agent_conversation_id`  | string  | Conversation ID for the agent interaction (empty string if not applicable) |
| `parent_conversation_id` | string  | Parent conversation ID linking related interview sessions                  |
| `engagement_id`          | string  | Engagement tracking ID (empty string if not applicable)                    |
| `posting_id`             | string  | The job posting ID                                                         |
| `timestamp`              | float   | Unix timestamp when the event was emitted                                  |
| `workflow_completed`     | boolean | Whether the full agentic workflow has been completed                       |
| `completion_timestamp`   | float   | Unix timestamp when the interview was actually completed                   |

***

### Payload: `report_available`

Sent when the AI-generated interview report is ready. This is the most comprehensive payload, containing the full analysis data: scores, highlights, scorecard, communication evaluation, transcript, interview data model, company info, and posting info.

```json theme={null}
{
  "interview_id": "A1B2C3D4E5F60718",
  "posting_id": "F9E8D7C6",
  "metadata": {
    "candidate_name": "Jane Doe",
    "candidate_email": "jane.doe@example.com",
    "candidate_id": "B4C5D6E7F8091A2B",
    "metadata": {
      "candidate_metadata_id": "550e8400-e29b-41d4-a716-446655440000",
      "resume_url": "https://example.cdn.heymilo.ai/WORKSPACE123/source/Jane_Doe_12345678.pdf"
    }
  },
  "report_url": "https://cdn.heymilo.ai/reports/MOCK_REPORT_HASH/jane-doe-2026-02-11.pdf",
  "timestamp": 1770817943.3959253,
  "interview_type": "web_interview",
  "agentic_data": {
    "web_interview_info": {
      "summary": {
        "interview_id": "A1B2C3D4E5F60718",
        "highlights": [
          "Demonstrated strong understanding of core technical concepts.",
          "Provided clear examples of past project experience with measurable outcomes.",
          "Communicated ideas concisely and showed good problem-solving approach."
        ],
        "lowlights": [
          "Did not elaborate on experience with distributed systems.",
          "Missed opportunity to connect leadership experience to the role requirements."
        ],
        "match_score": 78
      },
      "scorecard": {
        "interview_id": "A1B2C3D4E5F60718",
        "match_details": [
          "Demonstrated strong understanding of core technical concepts.",
          "Provided clear examples of past project experience with measurable outcomes.",
          "Did not elaborate on experience with distributed systems.",
          "Missed opportunity to connect leadership experience to the role requirements.",
          "Communicated ideas concisely and showed good problem-solving approach."
        ],
        "questions": [
          {
            "question_id": "Q1A2B3C4",
            "question": "Can you walk me through your professional background and key achievements in your recent roles?",
            "evaluation_summary": "Provided a clear career overview.\nMentioned specific companies and roles.\nDid not fully connect experience to this position.\nMissing some quantitative achievements.",
            "evaluation_criteria": "A good answer provides a clear, concise summary of relevant work experience, highlighting key roles, responsibilities, and achievements aligned with the position.",
            "preview_timestamp": 28,
            "evaluation_score": 7,
            "rank": 1,
            "chat_transcript": [
              {
                "speaker": "Candidate",
                "text": "Sure, I started my career at ...",
                "timestamp": 28,
                "timestamp_str": "00:28"
              }
            ],
            "score_of_1": "Very vague, unclear, or incomplete overview. Missing company names, roles, or timelines. No achievements or impact mentioned.",
            "score_of_5": "Gives a clear, chronological flow of career steps. Mentions companies and role titles. Provides relevant achievements and connects experience to the role.",
            "score_weight": 5,
            "not_scored": false
          },
          {
            "question_id": "Q5D6E7F8",
            "question": "Describe a challenging project you led and how you handled obstacles along the way.",
            "evaluation_summary": "Provided general insights on project challenges.\nArticulated team coordination well.\nCould have gone deeper on measurable impact.",
            "evaluation_criteria": "A good answer describes a specific project, the obstacles faced, actions taken, and outcomes achieved with clarity and structure.",
            "preview_timestamp": 274,
            "evaluation_score": 6,
            "rank": 2,
            "chat_transcript": [
              {
                "speaker": "Candidate",
                "text": "One of the most challenging projects was when ...",
                "timestamp": 274,
                "timestamp_str": "04:34"
              },
              {
                "speaker": "Candidate",
                "text": "We ended up restructuring the approach by ...",
                "timestamp": 418,
                "timestamp_str": "06:58"
              }
            ],
            "score_of_1": "A weak answer is vague, lacks specific details, and fails to describe actions taken or outcomes achieved.",
            "score_of_5": "A strong answer clearly describes the project, obstacles, actions, and outcomes with specific details and measurable results.",
            "score_weight": 5,
            "not_scored": false
          }
        ],
        "tags": [
          "Technical Skills",
          "Project Leadership",
          "Problem Solving"
        ]
      },
      "communication": {
        "interview_id": "A1B2C3D4E5F60718",
        "speech_score": -1,
        "evaluations": [],
        "template_id": null
      },
      "transcript": {
        "interview_id": "A1B2C3D4E5F60718",
        "transcript": [
          {
            "speaker": "bot",
            "text": "Hi, Jane Doe. Thanks for taking the time to meet with me. My name is Sarah, and I'm the AI interviewer. How is your day going so far?",
            "timestamp": 0,
            "timestamp_str": "00:00"
          },
          {
            "speaker": "user",
            "text": "Thank you. Have a nice day.",
            "timestamp": 827,
            "timestamp_str": "13:47"
          },
          {
            "speaker": "bot",
            "text": "Thank you for your time, Jane Doe. I appreciate you completing the interview.",
            "timestamp": 830,
            "timestamp_str": "13:50"
          }
        ]
      },
      "audio_recording_link": "https://cdn.heymilo.ai/audio/merged/1770817889-MOCK01/tmp_audio.wav",
      "video_recording_link": "https://cdn.heymilo.ai/videos/merged/1770817901-MOCK02/tmp_video.mp4"
    }
  },
  "interview_data_model": {
    "interview_id": "A1B2C3D4E5F60718",
    "details": {
      "interview_id": "A1B2C3D4E5F60718",
      "candidate_id": "B4C5D6E7F8091A2B",
      "interviewed_on": 1770817918.4658782,
      "score": 0,
      "name": "Jane Doe",
      "email": "jane.doe@example.com",
      "actions": null,
      "status": null,
      "workflow_status": {
        "interview_id": "A1B2C3D4E5F60718",
        "step_by_step": [
          {
            "complete": true,
            "required": false,
            "order": 1,
            "id": "web_interview",
            "started": true,
            "details": {
              "details": "Raw Interview Data Has Been Processed",
              "title": "Interview Processed Successfully",
              "description": "Candidate has attended the interview and completed it"
            },
            "raw_status": "INTERVIEW_PROCESSED_RAW_FILES",
            "last_updated_timestamp": 1770817918.4658782,
            "knocked_out": false,
            "completion_percentage": 1,
            "completion_metadata": null,
            "is_analyzing_call": false,
            "total_duration": null,
            "state_flags": {
              "is_completed": true,
              "is_incomplete": false,
              "is_analyzing_call": false,
              "is_in_progress": false
            }
          }
        ],
        "all_complete": true,
        "last_engagement": 1770817918.4658782,
        "metadata": null
      },
      "metadata": {
        "resume": null,
        "web_interview": {
          "final_score": 78,
          "skill_highlights": [
            "Technical Skills",
            "Project Leadership",
            "Problem Solving"
          ],
          "audio_recording_link": "https://cdn.heymilo.ai/audio/merged/1770817889-MOCK01/tmp_audio.wav",
          "video_recording_link": "https://cdn.heymilo.ai/videos/merged/1770817901-MOCK02/tmp_video.mp4"
        },
        "sms": null,
        "form": null
      },
      "candidate_feedback": null,
      "candidate_next_steps": null,
      "candidate_interview_url": "https://gointerview.co/example-company/i/A1B2C3D4E5F60718"
    },
    "company_info": {
      "company_id": "COMP1234",
      "workspace_id": "WS5678",
      "created_at": 1758332488.5218983,
      "updated_at": null,
      "created_by": "email|abc123def456",
      "name": "Example Corp",
      "image_url": "https://heymilo-west-0.s3.us-west-2.amazonaws.com/airstrip/WS5678/example-logo.png",
      "intro_message": null,
      "company_background": null,
      "voice_id": null,
      "knowledge_base": null,
      "interview_domain": null,
      "interview_domain_id": null,
      "sender_email_id": null,
      "address": null,
      "address_data": null,
      "timezone": null,
      "base_web_interview_config": {
        "backdrop_image_url": null,
        "avatar_image_url": null,
        "remove_heymilo_branding": null,
        "intro_message": null,
        "outro_message": null,
        "welcome_back_message": null
      },
      "message_settings": null,
      "sms_agent_config": null,
      "pdf_generation_config": null,
      "cheat_detection": null,
      "allow_white_labeling": null,
      "disable_all_candidate_communications": null,
      "interview_completion_confidence_threshold": 0.7,
      "retention_policy_days": null,
      "video_cheat_detection_enabled": null,
      "form_agent_enabled": null,
      "calendar_management_enabled": null,
      "email_templates_enabled": null,
      "phone_calls_enabled": null,
      "profile_picture_processing": null,
      "chat_agent_enabled": null,
      "copilot_enabled": null,
      "design_templates_enabled": null,
      "interview_templates_enabled": null,
      "sourcing_configs_enabled": null,
      "pbac_enabled": null,
      "email_report_to_candidate_enabled": null
    },
    "posting_info": {
      "posting_id": "F9E8D7C6",
      "title": "Senior Software Engineer",
      "name": "abc-def-123-456-789",
      "internal_name": null,
      "description": "<p><strong>About the Role</strong></p><p>We are looking for a Senior Software Engineer to join our growing team...</p>",
      "root_level_configs": {
        "instructions": "Conduct a structured technical interview with the candidate.",
        "company_overview": "Example Corp is a technology company building innovative solutions.",
        "job_overview": null,
        "interview_process_overview": null,
        "company_overview_delivery_mode": "summarized_delivery",
        "job_overview_delivery_mode": "exact_delivery",
        "interview_process_overview_delivery_mode": "exact_delivery",
        "language": "en",
        "interviewer_name": "Sarah",
        "is_multilingual": false
      },
      "company_id": "COMP1234",
      "created_at": 1769595148.8779235,
      "last_updated": 1769597734.2408419,
      "archived": false,
      "deadline": 1833235200,
      "finalized": true,
      "test_posting": false,
      "agentic_workflow": [
        {
          "type": "web_interview",
          "config": {
            "instructions": "Conduct a structured technical interview focusing on backend development and system design.",
            "company_overview": "Example Corp builds innovative cloud-based solutions for enterprise clients.",
            "job_overview": "The role involves designing and implementing scalable backend services.",
            "interview_process_overview": null,
            "company_overview_delivery_mode": null,
            "job_overview_delivery_mode": null,
            "interview_process_overview_delivery_mode": null,
            "language": "en",
            "interviewer_name": "Sarah",
            "is_multilingual": false,
            "video": true,
            "phone_calls_enabled": false,
            "voice_id": "voice-1",
            "interview_type": "voice",
            "prompt_version": "v2",
            "web_interview_config": {
              "backdrop_image_url": "",
              "avatar_image_url": "https://heymilo-west-0.s3.us-west-2.amazonaws.com/airstrip/MOCK/avatar.png",
              "remove_heymilo_branding": null,
              "intro_message": "Hi {{name}}, thanks for taking the time to meet with me! My name is {{interviewer_name}}, and I'm the AI interviewer. How is your day going so far?",
              "outro_message": "Thank you for your time {{name}}, I appreciate you completing the interview!",
              "welcome_back_message": "",
              "min_duration_seconds": null,
              "max_duration_seconds": null,
              "show_transcript": true,
              "candidate_presence_enabled": true,
              "vapi_call_config": {
                "end_call_message": "",
                "idle_message_max_spoken_count": null,
                "idle_timeout_seconds": 10,
                "wait_seconds": 2.4,
                "backoff_seconds": 5,
                "llm_model": null,
                "llm_temperature": 0.7
              }
            },
            "min_evaluation_score": 0,
            "num_questions": 5,
            "cheat_detection_config": {
              "enabled": false,
              "threshold": 70,
              "types": []
            }
          },
          "unique_id": null
        }
      ],
      "step_workflow": {
        "workflow": {
          "workflow_id": "wf_F9E8D7C6",
          "posting_id": "F9E8D7C6",
          "created_at": 1769595148.8779235
        },
        "steps": [
          {
            "step_id": "step_001",
            "workflow_id": "wf_F9E8D7C6",
            "step_type": "web_interview",
            "rank": 1,
            "details": "",
            "created_at": 1769595148.8779235,
            "depends_on": null
          }
        ]
      },
      "phone_number_id": null,
      "sender_email_id": "",
      "email_template_group_id": null,
      "design_template_group_id": null,
      "sourcing_config_ids": [],
      "redirect_url": null,
      "scheduling_url": null,
      "event_slug": null,
      "cal_team_id": null,
      "team_slug": null,
      "mascot_url": "https://heymilo-west-0.s3.us-west-2.amazonaws.com/avatars/mascot/COMP1234/F9E8D7C6/milo_mock.png",
      "mascot_id": "MASCOT123456",
      "days_to_complete": null,
      "ats_metadata": null,
      "allow_sms_comms": false,
      "max_retakes": null,
      "retake_cooldown_days": null,
      "is_template": false,
      "email_report_to_candidate": false
    },
    "web_interview_info": {
      "summary": {
        "interview_id": "A1B2C3D4E5F60718",
        "highlights": [
          "Demonstrated strong understanding of core technical concepts.",
          "Provided clear examples of past project experience with measurable outcomes.",
          "Communicated ideas concisely and showed good problem-solving approach."
        ],
        "lowlights": [
          "Did not elaborate on experience with distributed systems.",
          "Missed opportunity to connect leadership experience to the role requirements."
        ],
        "match_score": 78
      },
      "scorecard": {
        "interview_id": "A1B2C3D4E5F60718",
        "match_details": [
          "Demonstrated strong understanding of core technical concepts.",
          "Provided clear examples of past project experience with measurable outcomes.",
          "Did not elaborate on experience with distributed systems.",
          "Missed opportunity to connect leadership experience to the role requirements.",
          "Communicated ideas concisely and showed good problem-solving approach."
        ],
        "questions": [
          {
            "question_id": "Q1A2B3C4",
            "question": "Can you walk me through your professional background and key achievements in your recent roles?",
            "evaluation_summary": "Provided a clear career overview.\nMentioned specific companies and roles.\nDid not fully connect experience to this position.\nMissing some quantitative achievements.",
            "evaluation_criteria": "A good answer provides a clear, concise summary of relevant work experience.",
            "preview_timestamp": 28,
            "evaluation_score": 7,
            "rank": 1,
            "chat_transcript": [
              {
                "speaker": "Candidate",
                "text": "Sure, I started my career at ...",
                "timestamp": 28,
                "timestamp_str": "00:28"
              }
            ],
            "score_of_1": "Very vague, unclear, or incomplete overview.",
            "score_of_5": "Gives a clear, chronological flow of career steps with relevant achievements.",
            "score_weight": 5,
            "not_scored": false,
            "extracted_text": null
          }
        ],
        "tags": [
          "Technical Skills",
          "Project Leadership",
          "Problem Solving"
        ]
      },
      "communication": {
        "interview_id": "A1B2C3D4E5F60718",
        "speech_score": -1,
        "evaluations": [],
        "template_id": null
      },
      "transcript": {
        "interview_id": "A1B2C3D4E5F60718",
        "transcript": [
          {
            "speaker": "user",
            "text": "Thank you. Have a nice day.",
            "timestamp": 827,
            "timestamp_str": "13:47"
          },
          {
            "speaker": "bot",
            "text": "Thank you for your time, Jane Doe. I appreciate you completing the interview.",
            "timestamp": 830,
            "timestamp_str": "13:50"
          }
        ]
      },
      "audio_recording_link": "https://cdn.heymilo.ai/audio/merged/1770817889-MOCK01/tmp_audio.wav",
      "video_recording_link": "https://cdn.heymilo.ai/videos/merged/1770817901-MOCK02/tmp_video.mp4"
    },
    "resume_interview_info": null,
    "sms_interview_info": null,
    "form_interview_info": null
  }
}
```

#### `report_available` Top-Level Schema

| Field                  | Type   | Description                                                          |
| ---------------------- | ------ | -------------------------------------------------------------------- |
| `interview_id`         | string | Unique identifier for the interview                                  |
| `posting_id`           | string | The job posting ID                                                   |
| `metadata`             | object | Candidate metadata (same structure as `interview_started`)           |
| `report_url`           | string | Direct URL to download the generated PDF report                      |
| `timestamp`            | float  | Unix timestamp when the report became available                      |
| `interview_type`       | string | Type of interview (e.g., `web_interview`)                            |
| `agentic_data`         | object | AI-generated analysis data (summary, scorecard, transcript, etc.)    |
| `interview_data_model` | object | Full interview record with candidate details, company & posting info |

***

#### `agentic_data` Object

| Field                | Type        | Description                                                           |
| -------------------- | ----------- | --------------------------------------------------------------------- |
| `web_interview_info` | object/null | AI analysis of web/voice interview (present if interview type is web) |

#### `agentic_data.web_interview_info` Object

| Field                  | Type   | Description                                                  |
| ---------------------- | ------ | ------------------------------------------------------------ |
| `summary`              | object | High-level interview summary with highlights and match score |
| `scorecard`            | object | Detailed question-by-question scoring                        |
| `communication`        | object | Communication and speech quality evaluation                  |
| `transcript`           | object | Full interview transcript                                    |
| `audio_recording_link` | string | URL to the audio recording of the interview                  |
| `video_recording_link` | string | URL to the video recording (if video enabled)                |

#### `summary` Object

| Field          | Type    | Description                                    |
| -------------- | ------- | ---------------------------------------------- |
| `interview_id` | string  | Interview identifier                           |
| `highlights`   | array   | List of candidate strengths (strings)          |
| `lowlights`    | array   | List of candidate weaknesses or gaps (strings) |
| `match_score`  | integer | Overall match score (0-100)                    |

#### `scorecard` Object

| Field           | Type   | Description                                |
| --------------- | ------ | ------------------------------------------ |
| `interview_id`  | string | Interview identifier                       |
| `match_details` | array  | List of match assessment details (strings) |
| `questions`     | array  | List of scored question objects            |
| `tags`          | array  | Skill tags identified during the interview |

#### `scorecard.questions[]` Object

| Field                 | Type    | Description                                       |
| --------------------- | ------- | ------------------------------------------------- |
| `question_id`         | string  | Unique identifier for the question                |
| `question`            | string  | The interview question text                       |
| `evaluation_summary`  | string  | AI-generated summary of the candidate's answer    |
| `evaluation_criteria` | string  | The criteria used for evaluation                  |
| `preview_timestamp`   | integer | Timestamp offset (seconds) for the answer preview |
| `evaluation_score`    | integer | Score for this question (1-10)                    |
| `rank`                | integer | Ranking position of this question                 |
| `chat_transcript`     | array   | Relevant transcript excerpts for this question    |
| `score_of_1`          | string  | Description of what constitutes the lowest score  |
| `score_of_5`          | string  | Description of what constitutes the highest score |
| `score_weight`        | integer | Weight multiplier for this question's score       |
| `not_scored`          | boolean | Whether this question was excluded from scoring   |

#### `chat_transcript[]` Object

| Field           | Type    | Description                                      |
| --------------- | ------- | ------------------------------------------------ |
| `speaker`       | string  | Who spoke (`Candidate`, `bot`, `user`)           |
| `text`          | string  | The spoken text                                  |
| `timestamp`     | integer | Timestamp offset in seconds from interview start |
| `timestamp_str` | string  | Human-readable timestamp (e.g., `"04:34"`)       |

#### `communication` Object

| Field          | Type        | Description                                             |
| -------------- | ----------- | ------------------------------------------------------- |
| `interview_id` | string      | Interview identifier                                    |
| `speech_score` | float       | Overall speech/communication score (`-1` if not scored) |
| `evaluations`  | array       | List of communication evaluation objects (can be empty) |
| `template_id`  | string/null | Template ID used for evaluation (if applicable)         |

#### `transcript` Object

| Field          | Type   | Description                      |
| -------------- | ------ | -------------------------------- |
| `interview_id` | string | Interview identifier             |
| `transcript`   | array  | List of transcript entry objects |

***

#### `interview_data_model` Object

| Field                   | Type        | Description                                                 |
| ----------------------- | ----------- | ----------------------------------------------------------- |
| `interview_id`          | string      | Unique identifier for the interview                         |
| `details`               | object      | Core interview details (candidate info, workflow, metadata) |
| `company_info`          | object      | Full company/workspace configuration                        |
| `posting_info`          | object      | Full job posting configuration                              |
| `web_interview_info`    | object/null | Web interview analysis (duplicated from `agentic_data`)     |
| `resume_interview_info` | object/null | Resume screening analysis (`null` if no resume step)        |
| `sms_interview_info`    | object/null | SMS screening analysis (`null` if no SMS step)              |
| `form_interview_info`   | object/null | Form screening analysis (`null` if no form step)            |

#### `interview_data_model.details` Object

| Field                     | Type        | Description                                            |
| ------------------------- | ----------- | ------------------------------------------------------ |
| `interview_id`            | string      | Interview identifier                                   |
| `candidate_id`            | string      | Unique identifier for the candidate                    |
| `interviewed_on`          | float       | Unix timestamp when the interview was conducted        |
| `score`                   | float       | Overall score                                          |
| `name`                    | string      | Candidate's full name                                  |
| `email`                   | string      | Candidate's email address                              |
| `actions`                 | object/null | Available actions for this interview                   |
| `status`                  | string/null | Interview status                                       |
| `workflow_status`         | object      | Detailed workflow step statuses                        |
| `metadata`                | object      | Per-stage metadata (resume, web\_interview, sms, form) |
| `candidate_feedback`      | object/null | Candidate's feedback on the interview                  |
| `candidate_next_steps`    | object/null | Next steps for the candidate                           |
| `candidate_interview_url` | string      | The candidate-facing interview URL                     |

#### `workflow_status` Object

| Field             | Type        | Description                                      |
| ----------------- | ----------- | ------------------------------------------------ |
| `interview_id`    | string      | Interview identifier                             |
| `step_by_step`    | array       | List of workflow step status objects             |
| `all_complete`    | boolean     | Whether all required steps are complete          |
| `last_engagement` | float       | Unix timestamp of the last candidate interaction |
| `metadata`        | object/null | Additional workflow metadata                     |

#### `workflow_status.step_by_step[]` Object

| Field                    | Type        | Description                                                       |
| ------------------------ | ----------- | ----------------------------------------------------------------- |
| `complete`               | boolean     | Whether this step is complete                                     |
| `required`               | boolean     | Whether this step is required                                     |
| `order`                  | integer     | Position in the workflow sequence (1-based)                       |
| `id`                     | string      | Step identifier (e.g., `web_interview`, `resume_upload`)          |
| `started`                | boolean     | Whether the candidate has started this step                       |
| `details`                | object/null | Human-readable status details (`details`, `title`, `description`) |
| `raw_status`             | string      | Internal status code (e.g., `INTERVIEW_PROCESSED_RAW_FILES`)      |
| `last_updated_timestamp` | float       | Unix timestamp of the last update to this step                    |
| `knocked_out`            | boolean     | Whether the candidate was disqualified at this step               |
| `completion_percentage`  | float       | Step completion progress (0 to 1)                                 |
| `completion_metadata`    | object/null | Additional completion metadata                                    |
| `is_analyzing_call`      | boolean     | Whether AI analysis is still in progress                          |
| `total_duration`         | float/null  | Total duration of the step in seconds                             |
| `state_flags`            | object      | Boolean state flags for the step                                  |

#### `state_flags` Object

| Field               | Type    | Description                               |
| ------------------- | ------- | ----------------------------------------- |
| `is_completed`      | boolean | Whether the step is fully completed       |
| `is_incomplete`     | boolean | Whether the step is incomplete            |
| `is_analyzing_call` | boolean | Whether AI analysis is in progress        |
| `is_in_progress`    | boolean | Whether the step is currently in progress |

#### `details.metadata` Object

| Field           | Type        | Description                                                 |
| --------------- | ----------- | ----------------------------------------------------------- |
| `resume`        | object/null | Resume metadata (`null` if no resume step)                  |
| `web_interview` | object/null | Web interview metadata (score, highlights, recording links) |
| `sms`           | object/null | SMS screening metadata (`null` if no SMS step)              |
| `form`          | object/null | Form screening metadata (`null` if no form step)            |

#### `details.metadata.web_interview` Object

| Field                  | Type   | Description                  |
| ---------------------- | ------ | ---------------------------- |
| `final_score`          | float  | Final interview score        |
| `skill_highlights`     | array  | List of skill tags (strings) |
| `audio_recording_link` | string | URL to audio recording       |
| `video_recording_link` | string | URL to video recording       |

#### `company_info` Object

| Field                                       | Type         | Description                                               |
| ------------------------------------------- | ------------ | --------------------------------------------------------- |
| `company_id`                                | string       | Unique company identifier                                 |
| `workspace_id`                              | string       | Workspace identifier                                      |
| `created_at`                                | float        | Unix timestamp when the company was created               |
| `updated_at`                                | float/null   | Unix timestamp of last update                             |
| `created_by`                                | string       | Creator identifier                                        |
| `name`                                      | string       | Company name                                              |
| `image_url`                                 | string       | Company logo URL                                          |
| `intro_message`                             | string/null  | Custom intro message                                      |
| `company_background`                        | string/null  | Company background info                                   |
| `voice_id`                                  | string/null  | Default voice ID for interviews                           |
| `knowledge_base`                            | object/null  | Company knowledge base configuration                      |
| `interview_domain`                          | string/null  | Custom interview domain                                   |
| `interview_domain_id`                       | string/null  | Custom interview domain ID                                |
| `sender_email_id`                           | string/null  | Sender email configuration ID                             |
| `address`                                   | string/null  | Company address                                           |
| `address_data`                              | object/null  | Structured address data                                   |
| `timezone`                                  | string/null  | Company timezone                                          |
| `base_web_interview_config`                 | object       | Default web interview config (backdrop, avatar, branding) |
| `message_settings`                          | object/null  | Messaging configuration                                   |
| `sms_agent_config`                          | object/null  | SMS agent configuration                                   |
| `pdf_generation_config`                     | object/null  | PDF report generation settings                            |
| `cheat_detection`                           | object/null  | Cheat detection configuration                             |
| `allow_white_labeling`                      | boolean/null | Whether white labeling is enabled                         |
| `disable_all_candidate_communications`      | boolean/null | Whether all candidate communications are disabled         |
| `interview_completion_confidence_threshold` | float        | Confidence threshold for interview completion (0-1)       |
| `retention_policy_days`                     | integer/null | Data retention policy in days                             |
| `video_cheat_detection_enabled`             | boolean/null | Whether video cheat detection is enabled                  |
| `form_agent_enabled`                        | boolean/null | Whether form agent is enabled                             |
| `calendar_management_enabled`               | boolean/null | Whether calendar management is enabled                    |
| `email_templates_enabled`                   | boolean/null | Whether email templates are enabled                       |
| `phone_calls_enabled`                       | boolean/null | Whether phone calls are enabled                           |
| `profile_picture_processing`                | boolean/null | Whether profile picture processing is enabled             |
| `chat_agent_enabled`                        | boolean/null | Whether chat agent is enabled                             |
| `copilot_enabled`                           | boolean/null | Whether copilot is enabled                                |
| `design_templates_enabled`                  | boolean/null | Whether design templates are enabled                      |
| `interview_templates_enabled`               | boolean/null | Whether interview templates are enabled                   |
| `sourcing_configs_enabled`                  | boolean/null | Whether sourcing configs are enabled                      |
| `pbac_enabled`                              | boolean/null | Whether permissions-based access control is enabled       |
| `email_report_to_candidate_enabled`         | boolean/null | Whether email reports to candidates are enabled           |

#### `posting_info` Object

| Field                       | Type         | Description                                      |
| --------------------------- | ------------ | ------------------------------------------------ |
| `posting_id`                | string       | Unique posting identifier                        |
| `title`                     | string       | Job title                                        |
| `name`                      | string       | URL-friendly posting name                        |
| `internal_name`             | string/null  | Internal posting name                            |
| `description`               | string       | Job description (HTML)                           |
| `root_level_configs`        | object       | Top-level agent configuration                    |
| `company_id`                | string       | Company identifier                               |
| `created_at`                | float        | Unix timestamp when the posting was created      |
| `last_updated`              | float        | Unix timestamp when the posting was last updated |
| `archived`                  | boolean      | Whether the posting is archived                  |
| `deadline`                  | float        | Posting deadline (Unix timestamp)                |
| `finalized`                 | boolean      | Whether the posting is finalized                 |
| `test_posting`              | boolean      | Whether this is a test posting                   |
| `agentic_workflow`          | array        | List of agent workflow steps with configurations |
| `step_workflow`             | object       | Workflow definition with steps and dependencies  |
| `phone_number_id`           | string/null  | Associated phone number ID                       |
| `sender_email_id`           | string       | Sender email ID                                  |
| `email_template_group_id`   | string/null  | Email template group ID                          |
| `design_template_group_id`  | string/null  | Design template group ID                         |
| `sourcing_config_ids`       | array        | List of sourcing config IDs                      |
| `redirect_url`              | string/null  | Post-interview redirect URL                      |
| `scheduling_url`            | string/null  | Scheduling page URL                              |
| `event_slug`                | string/null  | Calendar event slug                              |
| `cal_team_id`               | string/null  | Calendar team ID                                 |
| `team_slug`                 | string/null  | Team slug                                        |
| `mascot_url`                | string       | Mascot image URL                                 |
| `mascot_id`                 | string       | Mascot identifier                                |
| `days_to_complete`          | integer/null | Days allowed to complete the interview           |
| `ats_metadata`              | object/null  | ATS integration metadata                         |
| `allow_sms_comms`           | boolean      | Whether SMS communications are enabled           |
| `max_retakes`               | integer/null | Maximum number of interview retakes              |
| `retake_cooldown_days`      | integer/null | Cooldown period between retakes                  |
| `is_template`               | boolean      | Whether this posting is a template               |
| `email_report_to_candidate` | boolean      | Whether to email reports to candidates           |

***

## Verifying Webhook Signatures

Webhooks registered through the v2 or v4 create endpoints are signed with HMAC-SHA256, so you can confirm the request genuinely came from HeyMilo and was not altered in transit. Verification is optional — deliveries succeed whether or not you check the signature — but it is strongly recommended for any endpoint that handles candidate data.

### Getting Your Signing Secret

When you register a webhook through `POST /api/v2/webhooks`, the create response includes a `signing_secret` beginning with `whsec_`, nested inside the `data` object:

```json theme={null}
{
  "data": {
    "object": "webhook",
    "webhook_id": "A1B2C3D4E5F60718293A4B5C6D7E8F90",
    "posting_id": "A1B2C3D4",
    "webhook_url": "https://your-domain.com/webhook-endpoint",
    "event_type": "interview_completed",
    "is_active": true,
    "signing_secret": "whsec_a1b2c3d4e5f6...",
    "created_at": 1703097600.0,
    "updated_at": 1703097600.0
  },
  "meta": null
}
```

`POST /api/v4/webhooks` returns the same object under `data`, alongside a top-level `"object": "webhook"`. With either endpoint, read the secret from `data.signing_secret`.

A few things to know about the secret:

* **It is returned only once.** The `signing_secret` appears in the create response and is never included in get or list responses. Store it securely as soon as you receive it.
* **It cannot be retrieved or rotated.** If you lose it, delete the webhook and register a new one to obtain a fresh secret.
* **Each webhook has its own secret.** Registering three webhooks for three event types gives you three distinct secrets.
* **It is returned by the v2 and v4 create endpoints only.** Use `POST /api/v2/webhooks` (Public API v2) or `POST /api/v4/webhooks`. The legacy `POST /api/webhook/create` endpoint does not return a signing secret.
* **Webhooks registered before signing was introduced have no secret.** Their deliveries are sent unsigned. To start receiving signed deliveries on an existing registration, delete it and create it again.

### Signature Headers

Signed deliveries carry two additional headers:

| Header                | Description                                                                         |
| --------------------- | ----------------------------------------------------------------------------------- |
| `X-HeyMilo-Signature` | The timestamp and signature, formatted as `t=<unix_seconds>,v1=<hex_digest>`        |
| `X-HeyMilo-Delivery`  | An identifier for the delivery — read the note below before using it to deduplicate |

What `X-HeyMilo-Delivery` identifies depends on the event type:

* For `interview_started`, `interview_completed` and `report_available`, it is a per-event notification ID. The value stays the same across retries of that event, so you can use it as a deduplication key.
* For `candidate_shortlisted` and `candidate_rejected`, it identifies the webhook registration rather than the individual event, so every delivery to that webhook carries the same value. Deduplicate these events on the payload instead.

**Example:**

```http theme={null}
X-HeyMilo-Signature: t=1703097600,v1=3f8a1c9e7b2d4f6a8c0e2b4d6f8a0c2e4b6d8f0a2c4e6b8d0f2a4c6e8b0d2f4a
X-HeyMilo-Delivery: 8F2A41C6B9D34E5F
Content-Type: application/json
User-Agent: HeyMilo-Webhooks/1.0
```

### How the Signature Is Computed

The signed string is the timestamp, a literal `.` character, and the raw request body:

```
signed_string = "{timestamp}.{raw_request_body}"
signature     = hex( HMAC-SHA256(signing_secret, signed_string) )
```

To verify a delivery:

1. Read `t` and `v1` from the `X-HeyMilo-Signature` header.
2. Concatenate `t`, a `.`, and the **raw** request body.
3. Compute the HMAC-SHA256 of that string using your signing secret and hex-encode it.
4. Compare the result against `v1` using a constant-time comparison.

<Warning>
  Always verify against the **raw request body bytes**, before any JSON parsing or re-serialisation. Re-encoding the JSON can reorder keys or change whitespace, which produces a different signature and causes verification to fail.
</Warning>

### Replay Protection

The timestamp is included in the signed string so that a captured request cannot be replayed indefinitely. HeyMilo does not impose a validity window — you choose the tolerance that suits your system. A common choice is to reject any delivery whose timestamp is more than **five minutes** away from your current time.

A timestamp check does not by itself prevent duplicate processing: a delivery that fails is retried with the same body, so the same event can arrive more than once. Deduplicate on a key derived from the payload — for example the interview or candidate identifier combined with the event type — or on `X-HeyMilo-Delivery` for the event types where it carries a per-event ID.

### Example: Node.js / Express

Use `express.raw()` for the webhook route so the original body is preserved.

```javascript theme={null}
const crypto = require("crypto");
const express = require("express");

const app = express();
const SIGNING_SECRET = process.env.HEYMILO_SIGNING_SECRET;
const TOLERANCE_SECONDS = 300;

function verifySignature(rawBody, headerValue, secret) {
  if (!headerValue) return false;

  const parts = Object.fromEntries(
    headerValue.split(",").map((kv) => kv.split("=")),
  );
  const timestamp = parts.t;
  const received = parts.v1;
  if (!timestamp || !received) return false;

  // Reject deliveries outside the tolerance window
  const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
  if (age > TOLERANCE_SECONDS) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  const a = Buffer.from(expected, "utf8");
  const b = Buffer.from(received, "utf8");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post(
  "/webhook/heymilo",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const rawBody = req.body.toString("utf8");

    if (!verifySignature(rawBody, req.get("X-HeyMilo-Signature"), SIGNING_SECRET)) {
      return res.status(400).json({ error: "Invalid signature" });
    }

    const event = JSON.parse(rawBody);
    processWebhookEvent(event);

    res.status(200).json({ received: true });
  },
);
```

### Example: Python / FastAPI

```python theme={null}
import hashlib
import hmac
import json
import os
import time

from fastapi import FastAPI, HTTPException, Request

app = FastAPI()

SIGNING_SECRET = os.environ["HEYMILO_SIGNING_SECRET"]
TOLERANCE_SECONDS = 300


def verify_signature(raw_body: bytes, header_value: str | None, secret: str) -> bool:
    if not header_value:
        return False

    try:
        parts = dict(kv.split("=", 1) for kv in header_value.split(","))
        timestamp = parts["t"]
        received = parts["v1"]
    except (ValueError, KeyError):
        return False

    # Reject deliveries outside the tolerance window
    if abs(int(time.time()) - int(timestamp)) > TOLERANCE_SECONDS:
        return False

    signed_content = f"{timestamp}.".encode("utf-8") + raw_body
    expected = hmac.new(
        secret.encode("utf-8"), signed_content, hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(expected, received)


@app.post("/webhook/heymilo")
async def handle_webhook(request: Request):
    raw_body = await request.body()

    if not verify_signature(raw_body, request.headers.get("X-HeyMilo-Signature"), SIGNING_SECRET):
        raise HTTPException(status_code=400, detail="Invalid signature")

    event = json.loads(raw_body)
    await process_webhook_event(event)

    return {"received": True}
```

***

## Setting Up Webhooks

### Step 1: Prepare Your Endpoint

Your webhook endpoint must be a publicly accessible URL that can receive HTTP requests. The endpoint should:

* Accept `POST` (or `GET`) requests
* Return a `200` status code to acknowledge receipt
* Process the payload asynchronously if heavy processing is required
* Verify the `X-HeyMilo-Signature` header before trusting the payload — see [Verifying Webhook Signatures](#verifying-webhook-signatures)

The examples below omit signature verification for brevity. Production endpoints should include it.

**Example endpoint (Node.js / Express):**

```javascript theme={null}
app.post("/webhook/heymilo", (req, res) => {
  const event = req.body;

  console.log(`Interview ID: ${event.interview_id}`);
  console.log(`Posting ID: ${event.posting_id}`);

  // Process the event asynchronously
  processWebhookEvent(event);

  // Acknowledge receipt immediately
  res.status(200).json({ received: true });
});
```

**Example endpoint (Python / FastAPI):**

```python theme={null}
from fastapi import FastAPI, Request

app = FastAPI()

@app.post("/webhook/heymilo")
async def handle_webhook(request: Request):
    event = await request.json()

    print(f"Interview ID: {event['interview_id']}")
    print(f"Posting ID: {event['posting_id']}")

    # Process the event
    await process_webhook_event(event)

    return {"received": True}
```

### Step 2: Register the Webhook

Use the Create Webhook API to register your endpoint for the desired event type and job posting.

```bash theme={null}
curl -X POST "https://api.heymilo.network/api/webhook/create" \
  -H "X-API-KEY: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "posting_id": "your_posting_id",
    "url": "https://your-domain.com/webhook/heymilo",
    "event_type": "interview_completed",
    "http_method": "post"
  }'
```

### Step 3: Verify Delivery

After registering, trigger a test event (e.g., start an interview for a test candidate) and confirm your endpoint receives the payload. Check the response status and payload structure to ensure your integration handles the data correctly.

***

## Multiple Webhooks Per Posting

You can register multiple webhooks for the same posting to subscribe to different event types or send events to different endpoints.

**Example: Subscribe to all events for a single posting**

```bash theme={null}
# Webhook for interview started
curl -X POST "https://api.heymilo.network/api/webhook/create" \
  -H "X-API-KEY: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "posting_id": "posting_123",
    "url": "https://your-domain.com/webhook/started",
    "event_type": "interview_started"
  }'

# Webhook for interview completed
curl -X POST "https://api.heymilo.network/api/webhook/create" \
  -H "X-API-KEY: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "posting_id": "posting_123",
    "url": "https://your-domain.com/webhook/completed",
    "event_type": "interview_completed"
  }'

# Webhook for report available
curl -X POST "https://api.heymilo.network/api/webhook/create" \
  -H "X-API-KEY: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "posting_id": "posting_123",
    "url": "https://your-domain.com/webhook/report",
    "event_type": "report_available"
  }'
```

***

## Best Practices

<details>
  <summary><strong>Respond Quickly</strong></summary>

  Return a `200` status code as soon as you receive the webhook. Perform any heavy processing (database writes, downstream API calls, etc.) asynchronously to avoid timeouts.
</details>

<details>
  <summary><strong>Handle Duplicate Events</strong></summary>

  Use the `interview_id` field to deduplicate events. In rare cases, the same event may be delivered more than once. Design your handler to be idempotent.
</details>

<details>
  <summary><strong>Validate Payloads</strong></summary>

  Always validate the structure of incoming payloads before processing. Check for required fields and handle missing or unexpected data gracefully. Many fields can be `null` depending on the interview configuration.
</details>

<details>
  <summary><strong>Use HTTPS Endpoints</strong></summary>

  Always use HTTPS URLs for your webhook endpoints to ensure data is encrypted in transit. HeyMilo sends sensitive candidate information in webhook payloads.
</details>

<details>
  <summary><strong>Verify Signatures</strong></summary>

  Check the `X-HeyMilo-Signature` header on every delivery before processing it, so that only genuine HeyMilo requests reach your handler. See [Verifying Webhook Signatures](#verifying-webhook-signatures) for the scheme and worked examples. Verify against the raw request body and use a constant-time comparison.
</details>

<details>
  <summary><strong>Monitor Webhook Health</strong></summary>

  Log all incoming webhook requests and monitor for failures. If your endpoint becomes unreachable, events may be missed. Consider implementing alerting for webhook processing errors.
</details>

<details>
  <summary><strong>Scope Webhooks to Postings</strong></summary>

  Register webhooks only for the job postings you need to monitor. This reduces noise and ensures your endpoint only receives relevant events.
</details>

<details>
  <summary><strong>Handle Nullable Fields</strong></summary>

  Many fields in the `report_available` payload can be `null` depending on the posting configuration (e.g., `resume_interview_info`, `sms_interview_info`, `form_interview_info`). Always check for `null` before accessing nested properties.
</details>

***

## Common Use Cases

<details>
  <summary><strong>ATS Sync</strong></summary>

  Use `interview_completed` or `report_available` webhooks to automatically push interview results back to your Applicant Tracking System. Extract the score, workflow status, and report data to update candidate records in real time.
</details>

<details>
  <summary><strong>Recruiter Notifications</strong></summary>

  Trigger Slack, email, or SMS notifications to recruiters when candidates complete interviews. Use the `interview_completed` event to alert team members and include the candidate name, score, and a link to the full report.
</details>

<details>
  <summary><strong>Automated Candidate Progression</strong></summary>

  Use `report_available` to automatically advance or reject candidates based on their scores. For example, candidates with a `match_score` above a threshold can be moved to the next stage, while those below can receive an automated rejection email.
</details>

<details>
  <summary><strong>Analytics & Dashboards</strong></summary>

  Stream webhook events into your analytics platform to build custom hiring dashboards. Track metrics like time-to-complete, average scores, and candidate throughput across postings.
</details>

<details>
  <summary><strong>Workflow Automation (Zapier / Make)</strong></summary>

  Use webhook URLs as triggers in Zapier, Make, or other automation platforms. This enables no-code workflows like adding candidates to Google Sheets, sending calendar invites, or updating CRM records when interviews are completed.
</details>

***

## Error Handling

If your endpoint returns a non-`200` status code, the webhook delivery is considered failed. Ensure your endpoint is reliable and returns `200` promptly.

### Common Errors

| Scenario               | Recommendation                                                        |
| ---------------------- | --------------------------------------------------------------------- |
| Endpoint returns `4xx` | Check your endpoint URL and ensure it accepts the correct HTTP method |
| Endpoint returns `5xx` | Investigate server-side errors in your application                    |
| Endpoint unreachable   | Verify your endpoint is publicly accessible and DNS is configured     |
| Payload parsing fails  | Validate the `Content-Type` header and JSON body parsing logic        |

***

## Support

For webhook support and questions:

* Email: [support@heymilo.ai](mailto:support@heymilo.ai)
* Documentation: [https://docs.heymilo.cloud](https://docs.heymilo.cloud)
* Status Page: [https://status.heymilo.ai](https://status.heymilo.ai)
