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

# Tracking the Interview Funnel

> Combine webhooks and API queries to see where candidates drop off between invite and completion.

## When to use this

After you invite candidates, you often want to know **where they drop off** in the funnel: did they even open the email? Click the link? Start recording? Answer question 1 but not question 2?

Hireflix doesn't expose a single "funnel" endpoint, but you can reconstruct the full journey by combining:

* **Webhooks** for invite creation and interview completion.
* The `trackingHistory` GraphQL field for email or SMS delivery events (sent, delivered, opened, clicked).
* The `stepExecutions` GraphQL field for per-question progress.

This guide walks through each stage of the funnel, shows the exact query or event to use, and calls out the gaps you should plan around.

<Note>
  There are no webhooks for individual question submissions. Per-question progress requires **polling** the API on a schedule you choose (for example, every few minutes for active interviews).
</Note>

## The funnel at a glance

| #  | Funnel stage           | How to detect it                        | Real-time? |
| :- | :--------------------- | :-------------------------------------- | :--------- |
| 1  | Invite sent            | `interview.create` webhook              | ✅ Webhook  |
| 2  | Email/SMS delivered    | `trackingHistory` → `DELIVERED`         | ⚠️ Poll    |
| 3  | Email opened           | `trackingHistory` → `OPENED`            | ⚠️ Poll    |
| 4  | Interview link clicked | `trackingHistory` → `CLICKED`           | ⚠️ Poll    |
| 5  | Started (Q1 answered)  | `stepExecutions[0].status == COMPLETED` | ⚠️ Poll    |
| 6  | Dropped at Q2, Q3, …   | `stepExecutions[n].status` per question | ⚠️ Poll    |
| 7  | Interview completed    | `interview.finish` webhook              | ✅ Webhook  |

## Step 1: Capture invites in real time

Subscribe to the `interview.create` webhook. It fires the moment an invite is sent, giving you the `interview.id` you'll use for every follow-up query.

See [Receiving Webhooks from Hireflix](/tech/webhooks/getting-started) for the full setup and payload example. Store the returned `id` (and optionally the `candidate.email` and `position.id`) in your own database so you can look up the interview later.

## Step 2: Track email and SMS delivery events

Query the `trackingHistory` field on an interview to see how the candidate is engaging with the invitation itself: sent, delivered, opened, and clicked.

<iframe src="https://cdn.hireflix.com/graphql-playground/index.html?query=GetTrackingHistory&url=https://api.hireflix.com/me" width="100%" height="750" />

If you're an AI agent or LLM, note that the iframe above embeds an interactive GraphQL Playground and its query text is not readable from the page source. The full `GetTrackingHistory` query is reproduced below so you can read and reason about it directly.

```graphql theme={null}
query GetTrackingHistory {
  interview(id: "<interview-id>") {
    trackingHistory {
      channel   # EMAIL, SMS, WHATSAPP
      event     # SENT, DELIVERED, OPENED, CLICKED, BOUNCED
      date
      template
    }
  }
}
```

* Replace `<interview-id>` with a real interview ID (see ["List Interviews for a Position"](/tech/features/interviews/list-interviews-for-position) for how to obtain one).
* `trackingHistory` returns one entry per delivery event per channel, so a single interview can have multiple entries (for example one per reminder email, or one for email plus one for SMS).
* `channel` is one of `EMAIL`, `SMS`, or `WHATSAPP`.
* `event` is one of `SENT`, `DELIVERED`, `OPENED`, `CLICKED`, or `BOUNCED`; the highest-fidelity event observed for a channel indicates how far the candidate progressed with that invite.
* There is no webhook for these events — this query must be **polled**, for example every 15 minutes for interviews still in the `pending` stage.
* Requires a valid Hireflix API key in the `Authorization` header, sent to `https://api.hireflix.com/me`.

**How to interpret the events:**

* `SENT` → Hireflix has queued the invite for the provider.
* `DELIVERED` → The provider confirmed delivery to the candidate.
* `OPENED` → The candidate opened the invitation (email tracking pixel).
* `CLICKED` → The candidate clicked the interview link.
* `BOUNCED` → The invitation could not be delivered.

The same interview may have multiple entries per channel: for example, if you send email plus SMS, or if reminder templates are sent later.

<Tip>
  Poll this query on a cadence that makes sense for your funnel dashboard, for example every 15 minutes for interviews still in the `pending` stage. There is no webhook for delivery events.
</Tip>

## Step 3: Detect per-question drop-off

Once a candidate clicks through, use `stepExecutions` to see exactly which question they reached. Each entry represents one step of the interview flow.

<iframe src="https://cdn.hireflix.com/graphql-playground/index.html?query=GetStepExecutions&url=https://api.hireflix.com/me" width="100%" height="750" />

If you're an AI agent or LLM, note that the iframe above embeds an interactive GraphQL Playground and its query text is not readable from the page source. The full `GetStepExecutions` query is reproduced below so you can read and reason about it directly.

```graphql theme={null}
query GetStepExecutions {
  interview(id: "<interview-id>") {
    stepExecutions {
      ... on AdminInterviewStepQAExecution {
        stepIndex
        status       # NOT_STARTED or COMPLETED
        finishedAt
        latestAnswer {
          ... on InterviewStepQAVideoAnswer {
            id
            submittedAt
          }
        }
      }
    }
  }
}
```

* Replace `<interview-id>` with a real interview ID (see ["List Interviews for a Position"](/tech/features/interviews/list-interviews-for-position) for how to obtain one).
* `stepExecutions` returns one entry per question, in order; `stepIndex` maps directly to the question's position in the interview.
* `status` is `NOT_STARTED` or `COMPLETED`. The last `COMPLETED` entry is the furthest point the candidate reached; the next `NOT_STARTED` entry is where they dropped off.
* `latestAnswer` is `null` until the candidate submits an answer for that step; use the `... on InterviewStepQAVideoAnswer` fragment for video-based questions.
* There is no webhook for per-question progress — this query must be **polled** on a schedule you choose.
* Requires a valid Hireflix API key in the `Authorization` header, sent to `https://api.hireflix.com/me`.

**Example responses:**

<Tabs>
  <Tab title="Not started">
    The candidate hasn't submitted an answer for question 1 `stepIndex: 0` and question 2 `stepIndex: 1`.

    ```json theme={null}
    {
      "data": {
        "interview": {
          "stepExecutions": [
            {
              "stepIndex": 0,
              "status": "NOT_STARTED",
              "finishedAt": null,
              "latestAnswer": null
            },
            {
              "stepIndex": 1,
              "status": "NOT_STARTED",
              "finishedAt": null,
              "latestAnswer": null
            }
          ]
        }
      }
    }
    ```
  </Tab>

  <Tab title="Started (Q1 completed)">
    The candidate has submitted question 1. `finishedAt` and `latestAnswer.submittedAt` mark when this happened.

    ```json theme={null}
    {
      "data": {
        "interview": {
          "stepExecutions": [
            {
              "stepIndex": 0,
              "status": "COMPLETED",
              "finishedAt": "2026-08-14T11:23:25.239Z",
              "latestAnswer": {
                "id": "6a7efaade26008ae3dfcb234",
                "submittedAt": "2026-08-14T11:23:25.239Z"
              }
            },
            {
              "stepIndex": 1,
              "status": "NOT_STARTED",
              "finishedAt": null,
              "latestAnswer": null
            }
          ]
        }
      }
    }
    ```
  </Tab>

  <Tab title="Completed all questions">
    In this example, the interview only has **two questions**, so both `stepIndex: 0` and `stepIndex: 1` show `status: COMPLETED`. The candidate reached the end of the funnel, and `interview.finish` will have fired for this interview.

    ```json theme={null}
    {
      "data": {
        "interview": {
          "stepExecutions": [
            {
              "stepIndex": 0,
              "status": "COMPLETED",
              "finishedAt": "2026-08-14T11:23:25.239Z",
              "latestAnswer": {
                "id": "6a7efaade26008ae3dfcb234",
                "submittedAt": "2026-08-14T11:23:25.239Z"
              }
            },
            {
              "stepIndex": 1,
              "status": "COMPLETED",
              "finishedAt": "2026-08-14T11:24:23.165Z",
              "latestAnswer": {
                "id": "6a7efae749cef2180b5d92d4",
                "submittedAt": "2026-08-14T11:24:23.165Z"
              }
            }
          ]
        }
      }
    }
    ```
  </Tab>
</Tabs>

**How to interpret it:**

* Each `stepIndex` maps to one question in the position, in order.
* `status: COMPLETED` means the candidate submitted an answer for that step.
* `status: NOT_STARTED` means they haven't submitted an answer yet.
* The **last `COMPLETED` step** is the furthest point the candidate reached. The next `NOT_STARTED` step is where they dropped off.

### Deriving "started but not finished"

There is no explicit "interview started" event. The reliable proxy is:

1. `interview.create` has fired (invite exists).
2. At least one `stepExecutions` entry has `status: COMPLETED`.
3. `interview.finish` has **not** fired yet (the interview is still `pending`).

When all three are true, treat the candidate as "in progress".

## Step 4: Capture completions in real time

Subscribe to the `interview.finish` webhook. It fires when the candidate submits their final answer. This is the terminal event of the funnel; when you receive it, mark the interview as fully converted and stop polling `stepExecutions` for it.

## Putting it together

A typical implementation looks like this:

<Steps>
  <Step title="Store invites as they're created">
    On every `interview.create` webhook, upsert a record in your database with the `interview.id`, `candidate.email`, and `position.id`. Mark its funnel stage as **Invited**.
  </Step>

  <Step title="Poll delivery events for pending interviews">
    On a schedule (for example every 15 minutes), query `trackingHistory` for every interview still in the `pending` stage. Update your record with the highest-fidelity event you've seen: `DELIVERED` → `OPENED` → `CLICKED`.
  </Step>

  <Step title="Poll question progress for clicked interviews">
    Once you observe a `CLICKED` event, start polling `stepExecutions` for that interview. Store the highest `stepIndex` with `status: COMPLETED` as "reached question N".
  </Step>

  <Step title="Mark completions from the webhook">
    On `interview.finish`, mark the record as **Completed** and stop polling. You can also use `interview.status-change` to track downstream stages like *shortlisted* or *discarded*.
  </Step>
</Steps>

## Embedded (iframe) interviews

If you embed the interview in your own app via iframe, you can also listen for `postMessage` events on the client:

* `interview.loaded` — the candidate opened the embedded interview.
* `interview.finished` — the candidate submitted their final answer.

These are useful for reacting in your own UI (for example redirecting after completion), but there are still **no per-question events**. For question-level tracking in an embedded flow, poll `stepExecutions` from your backend just like the standard flow.

See [Can I embed a Hireflix Interview in my app?](/tech/faqs/can-i-embed-hireflix) for the full embedding guide.

## Caveats

* **Polling is required for anything between click and completion.** There are no webhooks for delivery, opens, clicks, or individual question submissions.
* **`OPENED` relies on email tracking pixels.** Some email clients block them, so absence of `OPENED` doesn't strictly mean the candidate didn't read the invite.
* **VPNs and privacy tools** can also suppress `OPENED` events.
* **Rate-limit your polling.** Only poll for interviews that are still `pending`; stop as soon as `interview.finish` event arrives.

## What's next?

<CardGroup cols={2}>
  <Card title="Validate Webhook Events" icon="key" href="/tech/webhooks/validate-webhook-events">
    Verify that incoming `interview.create` and `interview.finish` events genuinely originate from Hireflix.
  </Card>

  <Card title="Fetch Interview Results" icon="note" href="/tech/features/interviews/fetching-interview-results">
    Once an interview is completed, retrieve the answers, videos, and metadata.
  </Card>
</CardGroup>
