> ## 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.

# How-to: Bulk Downloading Interview Videos

## When to use this

Use this when you need to **archive or export interview videos in bulk** — for example, backing up interviews before a retention window closes, moving recordings into your own storage, or running an analysis over a full set of answers. It combines queries already covered in this section — [listing interviews for a position](/tech/features/interviews/list-interviews-for-position) and [fetching interview results](/tech/features/interviews/fetching-interview-results) — into a script that walks every completed interview and saves each answer video to disk.

<Tip>
  **Only need a handful of videos?** You can download them one by one from the dashboard instead of running a script: open the **position**, click into any **completed interview**, and use the **Download video** button in the bottom-left corner. This works fine for a few interviews, but it's a manual, one-video-at-a-time process — for anything bigger, one of the scripts below is much faster.
</Tip>

## The script

Pick the tab that matches what you need to export — both require **Node.js 18+** (for native `fetch`).

<Tabs>
  <Tab title="Single Position">
    Downloads every completed interview video for **one position**, given its position ID. The script:

    1. Fetches every interview for the position, paginating automatically 100 at a time until it's collected them all, and skips any still `pending` (candidate hasn't completed it yet).
    2. For each completed interview, fetches the `stepExecutions` and pulls the signed video `url` from each `latestAnswer`, skipping steps with a deleted or missing answer.
    3. Streams each video to disk under `hireflix_videos/`, named `FirstName_LastName__01_QuestionTitle.mp4` — the two-digit prefix keeps answers in question order per candidate.

    Save this as `download_videos.js`.

    ```javascript download_videos.js theme={null}
    #!/usr/bin/env node
    /**
     * Bulk-download all interview videos for a given Hireflix position.
     *
     * Requirements: Node.js 18+ (for native fetch)
     * Usage:
     *   export HIREFLIX_API_KEY="<your-api-key>"
     *   node download_videos.js <position-id>
     */

    import fs from "fs";
    import path from "path";
    import { pipeline } from "stream/promises";

    const API_URL = "https://api.hireflix.com/me";
    const API_KEY = process.env.HIREFLIX_API_KEY;
    const POSITION_ID = process.argv[2];
    const OUTPUT_DIR = "hireflix_videos";

    if (!API_KEY) {
      console.error("Missing HIREFLIX_API_KEY environment variable.");
      process.exit(1);
    }

    if (!POSITION_ID) {
      console.error("Usage: node download_videos.js <position-id>");
      process.exit(1);
    }

    const HEADERS = {
      "Content-Type": "application/json",
      "X-Api-Key": API_KEY,
    };

    async function gql(query, variables = {}) {
      const res = await fetch(API_URL, {
        method: "POST",
        headers: HEADERS,
        body: JSON.stringify({ query, variables }),
      });

      if (!res.ok) {
        throw new Error(`HTTP ${res.status}: ${await res.text()}`);
      }

      const data = await res.json();
      if (data.errors) {
        throw new Error(JSON.stringify(data.errors, null, 2));
      }
      return data.data;
    }

    async function getInterviews(positionId) {
      const query = `
        query($id: String!, $lastCursor: String!) {
          position(id: $id) {
            name
            interviewList(input: { filter: {}, pagination: { limit: 100, lastCursor: $lastCursor }, sort: [] }) {
              lastCursor
              results {
                id
                stage
                candidate { firstName lastName email }
              }
            }
          }
        }
      `;

      let positionName = null;
      const interviews = [];
      let cursor = ""; // "" fetches the first page

      while (true) {
        const data = await gql(query, { id: positionId, lastCursor: cursor });
        positionName = data.position.name;
        const page = data.position.interviewList;
        interviews.push(...page.results);

        if (page.results.length === 0 || !page.lastCursor) break;
        cursor = page.lastCursor;
      }

      return { positionName, interviews };
    }

    async function getInterviewVideos(interviewId) {
      const query = `
        query($id: String!) {
          interview(id: $id) {
            stepExecutions {
              ... on AdminInterviewStepQAExecution {
                template { title }
                latestAnswer {
                  ... on InterviewStepQAVideoAnswer {
                    video { ... on VideoMedia { url mimeType } }
                  }
                  ... on InterviewStepQADeletedAnswer { __typename }
                }
              }
            }
          }
        }
      `;
      const data = await gql(query, { id: interviewId });
      return data.interview.stepExecutions;
    }

    function safeFilename(text) {
      return (text || "").replace(/[^a-zA-Z0-9_-]+/g, "_").replace(/^_+|_+$/g, "");
    }

    async function downloadVideo(url, destPath) {
      const res = await fetch(url);
      if (!res.ok) {
        throw new Error(`Failed to download ${url}: HTTP ${res.status}`);
      }
      await pipeline(res.body, fs.createWriteStream(destPath));
    }

    async function main() {
      fs.mkdirSync(OUTPUT_DIR, { recursive: true });

      const { positionName, interviews } = await getInterviews(POSITION_ID);
      console.log(`Position: ${positionName} — ${interviews.length} interview(s) found`);

      for (const interview of interviews) {
        if (interview.stage === "pending") continue; // candidate hasn't completed the interview yet

        const candName = safeFilename(
          `${interview.candidate.firstName}_${interview.candidate.lastName}`
        );
        const steps = await getInterviewVideos(interview.id);

        for (let i = 0; i < steps.length; i++) {
          const step = steps[i];
          const answer = step.latestAnswer;
          if (!answer || !answer.video) continue; // deleted or missing answer

          const title = safeFilename(step.template?.title || `question_${i + 1}`);
          const url = answer.video.url;
          const ext = (answer.video.mimeType || "").includes("mp4") ? ".mp4" : ".webm";
          const filename = `${candName}__${String(i + 1).padStart(2, "0")}_${title}${ext}`;
          const dest = path.join(OUTPUT_DIR, filename);

          console.log(`Downloading ${filename} ...`);
          await downloadVideo(url, dest);
        }
      }

      console.log("Done.");
    }

    main().catch((err) => {
      console.error(err);
      process.exit(1);
    });
    ```

    <Steps>
      <Step title="Create the script file">
        Never run a script before? Open a **plain-text editor** — not Word or Google Docs, since those add hidden formatting that breaks the file. A free option that works on Mac and Windows is [VS Code](https://code.visualstudio.com/); Notepad (Windows) or TextEdit (Mac, switch to *Format → Make Plain Text* first) also work.

        Copy the full script above, paste it into a new file, and save it as `download_videos.js` in a folder you'll remember, e.g. your Desktop. Make sure your editor doesn't silently add `.txt` to the filename — on Windows, pick **All Files** in the save dialog's file-type dropdown; on Mac, TextEdit adds `.txt` unless you type the full name including `.js` yourself.
      </Step>

      <Step title="Open a terminal">
        The terminal (or command line) is where you'll type the commands in the remaining steps.

        * **Mac:** press `Cmd + Space`, type `Terminal`, press Enter.
        * **Windows:** press the Start key, type `Command Prompt` (or `PowerShell`), press Enter.

        Then move into the folder where you saved the script — for example, if you saved it on your Desktop:

        ```bash theme={null}
        cd Desktop
        ```
      </Step>

      <Step title="Check your Node.js version">
        Native `fetch` requires Node 18+. Confirm with:

        ```bash theme={null}
        node -v
        ```
      </Step>

      <Step title="Set your API key">
        Export your [Hireflix API key](/tech/quickstart) as an environment variable:

        ```bash theme={null}
        export HIREFLIX_API_KEY="<your-api-key>"
        ```
      </Step>

      <Step title="Run the script">
        Pass the ID of the position you want to export as an argument (see [fetching positions](/tech/features/positions/fetching-positions) for how to find it):

        ```bash theme={null}
        node download_videos.js <position-id>
        ```

        The script logs each file as it downloads and prints `Done.` when finished.
      </Step>
    </Steps>

    #### Output

    Videos are saved to a `hireflix_videos/` folder created next to the script, one file per answered question:

    ```
    hireflix_videos/
    ├── Jane_Doe__01_Motivation.mp4
    ├── Jane_Doe__02_Experience.mp4
    ├── John_Smith__01_Motivation.mp4
    └── ...
    ```
  </Tab>

  <Tab title="Whole Account">
    Downloads every completed interview video across **every position in your account**. The script:

    1. Fetches every position in your account.
    2. For each position, fetches every interview in it, paginating automatically 100 at a time until it's collected them all, and skips any still `pending` (candidate hasn't completed it yet).
    3. For each completed interview, fetches the `stepExecutions` and pulls the signed video `url` from each `latestAnswer`, skipping steps with a deleted or missing answer.
    4. Streams each video to disk under `hireflix_videos/<Position Name>/`, named `FirstName_LastName__01_QuestionTitle.mp4` — one subfolder per position, so candidates with the same name in different positions don't collide.

    Save this as `download_videos.js`.

    ```javascript download_videos.js theme={null}
    #!/usr/bin/env node
    /**
     * Bulk-download every interview video across your Hireflix account.
     *
     * Requirements: Node.js 18+ (for native fetch)
     * Usage:
     *   export HIREFLIX_API_KEY="<your-api-key>"
     *   node download_videos.js
     */

    import fs from "fs";
    import path from "path";
    import { pipeline } from "stream/promises";

    const API_URL = "https://api.hireflix.com/me";
    const API_KEY = process.env.HIREFLIX_API_KEY;
    const OUTPUT_DIR = "hireflix_videos";

    if (!API_KEY) {
      console.error("Missing HIREFLIX_API_KEY environment variable.");
      process.exit(1);
    }

    const HEADERS = {
      "Content-Type": "application/json",
      "X-Api-Key": API_KEY,
    };

    async function gql(query, variables = {}) {
      const res = await fetch(API_URL, {
        method: "POST",
        headers: HEADERS,
        body: JSON.stringify({ query, variables }),
      });

      if (!res.ok) {
        throw new Error(`HTTP ${res.status}: ${await res.text()}`);
      }

      const data = await res.json();
      if (data.errors) {
        throw new Error(JSON.stringify(data.errors, null, 2));
      }
      return data.data;
    }

    async function getPositions() {
      const query = `
        query {
          positions {
            id
            name
          }
        }
      `;
      const data = await gql(query);
      return data.positions;
    }

    async function getInterviews(positionId) {
      const query = `
        query($id: String!, $lastCursor: String!) {
          position(id: $id) {
            interviewList(input: { filter: {}, pagination: { limit: 100, lastCursor: $lastCursor }, sort: [] }) {
              lastCursor
              results {
                id
                stage
                candidate { firstName lastName email }
              }
            }
          }
        }
      `;

      const interviews = [];
      let cursor = ""; // "" fetches the first page

      while (true) {
        const data = await gql(query, { id: positionId, lastCursor: cursor });
        const page = data.position.interviewList;
        interviews.push(...page.results);

        if (page.results.length === 0 || !page.lastCursor) break;
        cursor = page.lastCursor;
      }

      return interviews;
    }

    async function getInterviewVideos(interviewId) {
      const query = `
        query($id: String!) {
          interview(id: $id) {
            stepExecutions {
              ... on AdminInterviewStepQAExecution {
                template { title }
                latestAnswer {
                  ... on InterviewStepQAVideoAnswer {
                    video { ... on VideoMedia { url mimeType } }
                  }
                  ... on InterviewStepQADeletedAnswer { __typename }
                }
              }
            }
          }
        }
      `;
      const data = await gql(query, { id: interviewId });
      return data.interview.stepExecutions;
    }

    function safeFilename(text) {
      return (text || "").replace(/[^a-zA-Z0-9_-]+/g, "_").replace(/^_+|_+$/g, "");
    }

    async function downloadVideo(url, destPath) {
      const res = await fetch(url);
      if (!res.ok) {
        throw new Error(`Failed to download ${url}: HTTP ${res.status}`);
      }
      await pipeline(res.body, fs.createWriteStream(destPath));
    }

    async function main() {
      fs.mkdirSync(OUTPUT_DIR, { recursive: true });

      const positions = await getPositions();
      console.log(`Found ${positions.length} position(s) in your account.`);

      for (const position of positions) {
        const interviews = await getInterviews(position.id);
        console.log(`\nPosition: ${position.name} — ${interviews.length} interview(s) found`);

        const positionDir = path.join(OUTPUT_DIR, safeFilename(position.name || position.id));
        fs.mkdirSync(positionDir, { recursive: true });

        for (const interview of interviews) {
          if (interview.stage === "pending") continue; // candidate hasn't completed the interview yet

          const candName = safeFilename(
            `${interview.candidate.firstName}_${interview.candidate.lastName}`
          );
          const steps = await getInterviewVideos(interview.id);

          for (let i = 0; i < steps.length; i++) {
            const step = steps[i];
            const answer = step.latestAnswer;
            if (!answer || !answer.video) continue; // deleted or missing answer

            const title = safeFilename(step.template?.title || `question_${i + 1}`);
            const url = answer.video.url;
            const ext = (answer.video.mimeType || "").includes("mp4") ? ".mp4" : ".webm";
            const filename = `${candName}__${String(i + 1).padStart(2, "0")}_${title}${ext}`;
            const dest = path.join(positionDir, filename);

            console.log(`Downloading ${filename} ...`);
            await downloadVideo(url, dest);
          }
        }
      }

      console.log("\nDone.");
    }

    main().catch((err) => {
      console.error(err);
      process.exit(1);
    });
    ```

    <Steps>
      <Step title="Create the script file">
        Never run a script before? Open a **plain-text editor** — not Word or Google Docs, since those add hidden formatting that breaks the file. A free option that works on Mac and Windows is [VS Code](https://code.visualstudio.com/); Notepad (Windows) or TextEdit (Mac, switch to *Format → Make Plain Text* first) also work.

        Copy the full script above, paste it into a new file, and save it as `download_videos.js` in a folder you'll remember, e.g. your Desktop. Make sure your editor doesn't silently add `.txt` to the filename — on Windows, pick **All Files** in the save dialog's file-type dropdown; on Mac, TextEdit adds `.txt` unless you type the full name including `.js` yourself.
      </Step>

      <Step title="Open a terminal">
        The terminal (or command line) is where you'll type the commands in the remaining steps.

        * **Mac:** press `Cmd + Space`, type `Terminal`, press Enter.
        * **Windows:** press the Start key, type `Command Prompt` (or `PowerShell`), press Enter.

        Then move into the folder where you saved the script — for example, if you saved it on your Desktop:

        ```bash theme={null}
        cd Desktop
        ```
      </Step>

      <Step title="Check your Node.js version">
        Native `fetch` requires Node 18+. Confirm with:

        ```bash theme={null}
        node -v
        ```
      </Step>

      <Step title="Set your API key">
        Export your [Hireflix API key](/tech/quickstart) as an environment variable:

        ```bash theme={null}
        export HIREFLIX_API_KEY="<your-api-key>"
        ```
      </Step>

      <Step title="Run the script">
        ```bash theme={null}
        node download_videos.js
        ```

        No arguments needed — it walks every position in your account automatically. The script logs each file as it downloads and prints `Done.` when finished.
      </Step>
    </Steps>

    #### Output

    Videos are saved to a `hireflix_videos/` folder created next to the script, with one subfolder per position:

    ```
    hireflix_videos/
    ├── Social_Media_Manager/
    │   ├── Jane_Doe__01_Motivation.mp4
    │   ├── Jane_Doe__02_Experience.mp4
    │   └── John_Smith__01_Motivation.mp4
    ├── Backend_Engineer/
    │   └── ...
    └── ...
    ```
  </Tab>
</Tabs>

<Warning>
  Video `url`s are **signed and expire after 14 days**. Both scripts always fetch a fresh URL right before downloading, so that's not an issue when you run them — but don't cache or reuse URLs from a previous run.
</Warning>

<Note>
  For every position it processes, the script paginates through **all** of its interviews (100 per request, looping on `lastCursor` until a page comes back empty) — it isn't capped at the first 100 candidates. The first request for each position must send `lastCursor: ""` (not `null`) — an explicit `null` causes the API to scope the result set incorrectly.

  Running the whole-account version can mean a lot of requests and large downloads, so it's also worth checking your data retention / GDPR policy before archiving candidate video off-platform.
</Note>

<Card title="Learn next?" icon="arrow-right" color="#5863ff" horizontal href="/tech/webhooks/getting-started">
  Let's learn how to create and use webhooks.
</Card>
