> ## 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: Creating a Position

> Create a new position (interview template) via the Hireflix API: name, tags, questions, outro, and video content.

## When to use this

A **position** is an interview template — it holds a name, one or more questions (steps), an optional outro, and optional intro/outro/question videos. Use `createPosition` whenever you want to set up a new role programmatically instead of using the Hireflix dashboard. Once a position exists, you invite candidates to it to actually create interviews (see [Inviting a Candidate](/tech/features/interviews/inviting-candidate)).

Videos are optional — you can attach an intro, outro, and/or per-question video, covered in [Adding video content to a position](#adding-video-content-to-a-position) below.

<Tip>
  If you want a **unique video for every question** on every position, it's usually faster to build that position from the Hireflix dashboard — recording and uploading several question videos per position through the API means several reserve-upload round trips per position.

  The API really shines for **bulk-creating positions or interviews** where you don't need a bespoke video per question. A common pattern: record **one intro and one outro video**, upload each **once**, and reuse their `mediaId` across every position you create via the API. You still get a personal-feeling candidate experience (a real face on the intro/outro) without re-uploading video per position.

  **-> Want to run this entirely from a script or backend server? See [Bulk-Creating Positions with a Script](/tech/features/positions/bulk-creating-positions).**
</Tip>

## Before you start

Don't forget to send your [Hireflix API Key](/tech/quickstart) in the `X-API-KEY` header to `https://api.hireflix.com/me`. Unlike updating or archiving a position, creating one needs no prior IDs — `createPosition` is often the very first mutation you'll call against the API.

## GraphQL Playground

<iframe src="https://cdn.hireflix.com/graphql-playground/index.html?query=CreatePosition&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. It's preloaded with the basic example — the same `CreatePosition` mutation shown in the **Basic** tab below. See that tab (and the other tabs in the **Create examples** section) for the full mutations and field notes covering every scenario.

## Create examples

Each tab below shows a variation of the same `createPosition` mutation, from a minimal position to one with tags, an outro, and multiple questions.

<Tabs>
  <Tab title="Basic">
    The smallest valid position: a name and one question. `answerFormat: { video: {} }` uses the default recording settings (120s max duration, 30s to think, no retakes).

    ```graphql theme={null}
    mutation CreatePosition {
      createPosition(input: {
        name: "Junior Developer"
        steps: [
          {
            QA: {
              title: "Tell us about yourself"
              answerFormat: {
                video: {}
              }
            }
          }
        ]
      }) {
        ... on MutationCreatePositionSuccess {
          data {
            id
            name
          }
        }
        ... on ValidationError {
          message
          fieldErrors { path message }
        }
      }
    }
    ```

    * **name** – required, 1–110 characters.
    * **steps** – required, at least 1 entry (max 50). Each `QA` step needs a `title` and an `answerFormat`.
    * **answerFormat.video** – required for a QA step. An empty object `{}` just means "use the defaults" — no custom duration, thinking time, or retakes.
  </Tab>

  <Tab title="With tags & outro">
    Adds `tags` for categorization and a custom `outro` screen shown to candidates after they finish.

    ```graphql theme={null}
    mutation CreatePosition {
      createPosition(input: {
        name: "Senior DX Researcher"
        tags: ["DX", "DevRel"]
        steps: [
          {
            QA: {
              title: "Tell us about yourself"
              answerFormat: {
                video: {
                  maxDurationSeconds: 120
                  timeToThinkSeconds: 20
                  numAllowedRetakes: 0
                }
              }
            }
          }
        ]
        outro: {
          title: "Thank you for applying!"
          subtitle: "We'll reach out in 5 business days."
        }
      }) {
        ... on MutationCreatePositionSuccess {
          data {
            id
            name
          }
        }
        ... on ValidationError {
          message
          fieldErrors { path message }
        }
      }
    }
    ```

    * **tags** – up to 5 tags, each 1–25 characters.
    * **answerFormat.video** – here the defaults are set explicitly instead of left as `{}`: `maxDurationSeconds` (60–900), `timeToThinkSeconds` (-1 to 600, `-1` disables the countdown), `numAllowedRetakes` (-1 to 100, `-1` unlimited, `0` none). If you're not sure what these should be, [Tailoring the Interview](/user/building-the-interview/tailoring-the-interview) explains what each setting means for the candidate (answering time limit, thinking time, retakes) from the dashboard's point of view — the same concepts, just named slightly differently in the UI.
    * **outro.title** / **outro.subtitle** – both required once you include `outro` at all. Note the input field is `subtitle`, but it comes back as `description` when you query it (see the next tab).
  </Tab>

  <Tab title="Full example">
    Combines tags, an outro, **two** questions with different settings, and an `introVideo` that references an **existing** `mediaId`.

    <Warning>
      This example does not upload a new video — `introVideo.mediaId` assumes you already have a `mediaId` from a previous upload. To upload a brand-new video and get a `mediaId`, see [Adding video content to a position](#adding-video-content-to-a-position) below.
    </Warning>

    ```graphql theme={null}
    mutation CreatePosition {
      createPosition(input: {
        name: "My Position"
        tags: ["DevRel", "DX"]
        introVideo: {
          mediaId: "6a8817e6c7f9fbb562980914"
        }
        outro: {
          title: "Thanks for applying!"
          subtitle: "We'll be in touch soon."
        }
        steps: [
          {
            QA: {
              title: "Tell us about yourself"
              answerFormat: {
                video: {}
              }
            }
          }
          {
            QA: {
              title: "Why do you want this role?"
              answerFormat: {
                video: {
                  maxDurationSeconds: 300
                  timeToThinkSeconds: 60
                  numAllowedRetakes: -1
                }
              }
            }
          }
        ]
      }) {
        ... on MutationCreatePositionSuccess {
          data {
            id
            name
            tags
            outro {
              title
              description
            }
          }
        }
        ... on ValidationError {
          message
          fieldErrors { path message }
        }
      }
    }
    ```

    * **steps** accepts multiple `QA` entries — each one becomes a separate question, played in order.
    * The second question sets `numAllowedRetakes: -1` (unlimited retakes) and a longer `maxDurationSeconds`/`timeToThinkSeconds` than the first, which uses the defaults (`{}`).
    * **introVideo.mediaId** references a video that was **already uploaded** — this mutation itself doesn't upload anything. Keep reading below to see how to get a real `mediaId`.
  </Tab>
</Tabs>

<Note>
  None of the examples above upload a new video — `mediaId` fields only **reference** media that already exists in your account. To upload a video and obtain a real `mediaId`, follow [Adding video content to a position](#adding-video-content-to-a-position) below, then come back and drop the returned `mediaId` into `introVideo`, `outro.video`, or a step's `questionFormat.video`.
</Note>

<Tip>
  You can always verify what you created by opening [admin.hireflix.com/jobs](https://admin.hireflix.com/jobs) and searching for the position by name.
</Tip>

## Handling the response

`createPosition` returns a union type — always branch on the possible cases:

* **`MutationCreatePositionSuccess`** – the position was created; read it from `data`.
* **`ValidationError`** – one or more fields failed validation; inspect `fieldErrors` for the offending `path` and `message`.
* **`MediaNotFoundError`** – a `mediaId` you referenced (e.g. in `introVideo`) doesn't exist, doesn't belong to your company, or its upload reservation was never completed.
* **`MediaFileTooLargeError`** / **`MediaFileUploadMimeTypeNotSupportedError`** / **`MediaMimeTypeMismatchError`** – only relevant if you're referencing a media asset with the wrong type or size for where it's used.
* **`ThemeNotFoundError`** / **`UsersNotFoundError`** / **`TemplatesNotFoundError`** – the `themeId`, `users`, or notification `template` IDs you passed don't exist or aren't accessible to you.

## Key reminders

* **`name`** is required, 1–110 characters.
* **`steps`** is required — at least 1, max 50. Each step currently supports the `QA` type.
* **`public`** defaults to `true` if omitted.
* **`mediaId` fields never upload anything** — they only reference media created via a `reservePosition*VideoUpload` mutation (see below).
* Verify a position was created by checking [admin.hireflix.com/jobs](https://admin.hireflix.com/jobs) and searching by name.
* Unsure what `maxDurationSeconds`, `timeToThinkSeconds`, or `numAllowedRetakes` should be set to? See [Tailoring the Interview](/user/building-the-interview/tailoring-the-interview) for what each setting means for candidates.

## Adding video content to a position

A position can show a video in three places: the **intro** screen, the **outro** screen, and a **QA question** prompt. All three follow the same three-step flow:

1. **Reserve** an upload with the matching mutation for that destination.
2. **PUT** the video file to the returned `presignedPutUrl`.
3. **Pass the returned `mediaId`** into `createPosition` (or [`updatePosition`](/tech/features/positions/updating-position) for an existing position).

<Tip>
  Per-question videos are usually easiest to record and upload from the dashboard. If you're bulk-creating positions via the API, consider uploading just **one intro and one outro video** and reusing their `mediaId` across every position — same personal touch, no re-upload per position.
</Tip>

| Destination       | Reserve mutation                      |
| ----------------- | ------------------------------------- |
| Intro video       | `reservePositionIntroVideoUpload`     |
| Outro video       | `reservePositionOutroVideoUpload`     |
| QA question video | `reservePositionQAVideoContentUpload` |

All three take the same input — `mimeType` (must match `video/*`, e.g. `"video/mp4"`) and `sizeBytes` (max 157286400 bytes / 150 MB) — and return the same shape: `{ mediaId, presignedPutUrl, expiresAt }`. `expiresAt` is roughly **1 hour** from the reservation — upload before then, or reserve again.

### Uploading the file

Once you have a `presignedPutUrl`, PUT the raw file to it with a matching `Content-Type`:

```bash theme={null}
curl -X PUT '<presignedPutUrl>' \
  -H "Content-Type: video/mp4" \
  --upload-file "/path/to/video.mp4"
```

<Warning>
  **Common pitfalls:**

  * **Always single-quote the URL.** Some shells (zsh in particular, via `url-quote-magic`) will auto-insert backslashes before `?`, `=`, and `&` when you paste an unquoted or double-quoted URL, silently corrupting the signature and producing an `AccessDenied` error. Wrapping it in single quotes (`'...'`) avoids this entirely.
  * **`Content-Type` must exactly match** the `mimeType` you declared when reserving the upload.
  * **Upload before `expiresAt`.** If it expires, re-run the reserve mutation for a fresh URL.
  * A successful PUT to the presigned URL returns an empty `200 OK` body — check the HTTP status, not the response content.
</Warning>

With the file uploaded, the `mediaId` from step 1 is ready to use. The tabs below show each reserve mutation paired with the `createPosition` call that uses its `mediaId`, followed by a full example combining all three.

<Tabs>
  <Tab title="Intro video">
    **1. Reserve the upload:**

    ```graphql theme={null}
    mutation ReserveIntroVideo {
      reservePositionIntroVideoUpload(input: {
        mimeType: "video/mp4"
        sizeBytes: 10485760
      }) {
        ... on MutationReservePositionIntroVideoUploadSuccess {
          data {
            mediaId
            presignedPutUrl
            expiresAt
          }
        }
        ... on ValidationError {
          validationMessage: message
          fieldErrors { path message }
        }
        ... on MediaFileTooLargeError { tooLargeMessage: message }
        ... on MediaFileUploadMimeTypeNotSupportedError { mimeTypeMessage: message }
      }
    }
    ```

    **2. PUT the file** to `presignedPutUrl` (see [Uploading the file](#uploading-the-file) above).

    **3. Use the `mediaId` in `createPosition`:**

    ```graphql theme={null}
    mutation CreatePosition {
      createPosition(input: {
        name: "Senior DX Researcher"
        introVideo: {
          mediaId: "<mediaId-from-step-1>"
        }
        steps: [
          {
            QA: {
              title: "Tell us about yourself"
              answerFormat: { video: {} }
            }
          }
        ]
      }) {
        ... on MutationCreatePositionSuccess {
          data {
            id
            name
            intro { id url }
          }
        }
        ... on MediaNotFoundError { notFoundMessage: message }
        ... on ValidationError {
          validationMessage: message
          fieldErrors { path message }
        }
      }
    }
    ```
  </Tab>

  <Tab title="Outro video">
    **1. Reserve the upload:**

    ```graphql theme={null}
    mutation ReserveOutroVideo {
      reservePositionOutroVideoUpload(input: {
        mimeType: "video/mp4"
        sizeBytes: 10485760
      }) {
        ... on MutationReservePositionOutroVideoUploadSuccess {
          data {
            mediaId
            presignedPutUrl
            expiresAt
          }
        }
        ... on ValidationError {
          validationMessage: message
          fieldErrors { path message }
        }
        ... on MediaFileTooLargeError { tooLargeMessage: message }
        ... on MediaFileUploadMimeTypeNotSupportedError { mimeTypeMessage: message }
      }
    }
    ```

    **2. PUT the file** to `presignedPutUrl` (see [Uploading the file](#uploading-the-file) above).

    **3. Use the `mediaId` in `createPosition`:**

    <Note>
      `outro.title` and `outro.subtitle` are both required as soon as you include `outro`, even if all you want to change is the video.
    </Note>

    ```graphql theme={null}
    mutation CreatePosition {
      createPosition(input: {
        name: "Senior DX Researcher"
        outro: {
          title: "Thanks for applying!"
          subtitle: "We'll be in touch soon."
          video: {
            mediaId: "<mediaId-from-step-1>"
          }
        }
        steps: [
          {
            QA: {
              title: "Tell us about yourself"
              answerFormat: { video: {} }
            }
          }
        ]
      }) {
        ... on MutationCreatePositionSuccess {
          data {
            id
            name
            outro {
              title
              description
              media { id url }
            }
          }
        }
        ... on MediaNotFoundError { notFoundMessage: message }
        ... on ValidationError {
          validationMessage: message
          fieldErrors { path message }
        }
      }
    }
    ```
  </Tab>

  <Tab title="QA question video">
    **1. Reserve the upload:**

    ```graphql theme={null}
    mutation ReserveQAVideoContent {
      reservePositionQAVideoContentUpload(input: {
        mimeType: "video/mp4"
        sizeBytes: 10485760
      }) {
        ... on MutationReservePositionQAVideoContentUploadSuccess {
          data {
            mediaId
            presignedPutUrl
            expiresAt
          }
        }
        ... on ValidationError {
          validationMessage: message
          fieldErrors { path message }
        }
        ... on MediaFileTooLargeError { tooLargeMessage: message }
        ... on MediaFileUploadMimeTypeNotSupportedError { mimeTypeMessage: message }
      }
    }
    ```

    **2. PUT the file** to `presignedPutUrl` (see [Uploading the file](#uploading-the-file) above).

    **3. Use the `mediaId` as the step's question content:**

    ```graphql theme={null}
    mutation CreatePosition {
      createPosition(input: {
        name: "Senior DX Researcher"
        steps: [
          {
            QA: {
              title: "Watch this and respond"
              questionFormat: {
                video: {
                  mediaId: "<mediaId-from-step-1>"
                }
              }
              answerFormat: { video: {} }
            }
          }
        ]
      }) {
        ... on MutationCreatePositionSuccess {
          data {
            id
            name
            stepTemplates(input: {}) {
              ... on AdminQAStep {
                id
                title
                questionFormat {
                  ... on AdminQAVideoContent {
                    media { id url }
                  }
                }
              }
            }
          }
        }
        ... on MediaNotFoundError { notFoundMessage: message }
        ... on ValidationError {
          validationMessage: message
          fieldErrors { path message }
        }
      }
    }
    ```

    * `questionFormat.video.mediaId` is the **question prompt** (what the candidate watches before answering) — it's separate from `answerFormat`, which controls the candidate's recorded response.
  </Tab>

  <Tab title="Full example">
    <Warning>
      This assumes you've already run **three separate reserve calls** — one each via `reservePositionIntroVideoUpload`, `reservePositionOutroVideoUpload`, and `reservePositionQAVideoContentUpload` — uploaded each file, and are using their three distinct `mediaId` values below. A `mediaId` reserved for one destination is not interchangeable with another.
    </Warning>

    ```graphql theme={null}
    mutation CreatePosition {
      createPosition(input: {
        name: "Senior DX Researcher"
        tags: ["DX", "DevRel"]
        introVideo: {
          mediaId: "<intro-mediaId>"
        }
        outro: {
          title: "Thanks for applying!"
          subtitle: "We'll be in touch soon."
          video: {
            mediaId: "<outro-mediaId>"
          }
        }
        steps: [
          {
            QA: {
              title: "Watch this and respond"
              questionFormat: {
                video: {
                  mediaId: "<qa-question-mediaId>"
                }
              }
              answerFormat: {
                video: {
                  maxDurationSeconds: 180
                  timeToThinkSeconds: 30
                  numAllowedRetakes: 1
                }
              }
            }
          }
        ]
      }) {
        ... on MutationCreatePositionSuccess {
          data {
            id
            name
            tags
            intro { id url }
            outro {
              title
              description
              media { id url }
            }
            stepTemplates(input: {}) {
              ... on AdminQAStep {
                id
                title
                questionFormat {
                  ... on AdminQAVideoContent {
                    media { id url }
                  }
                }
              }
            }
          }
        }
        ... on MediaNotFoundError { notFoundMessage: message }
        ... on ValidationError {
          validationMessage: message
          fieldErrors { path message }
        }
      }
    }
    ```
  </Tab>
</Tabs>

<Tip>
  Open [admin.hireflix.com/jobs](https://admin.hireflix.com/jobs), find the position by name, and preview it to confirm the intro/outro/question videos play back correctly.
</Tip>

<Card title="Learn next?" color="#5863ff" icon="arrow-right" horizontal href="/tech/features/positions/bulk-creating-positions">
  Need to create many positions at once? Let's learn how to bulk-create positions with a script.
</Card>
