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

# API v2 Reference

> The v2 public API, optimized for the v3 interview engine

# API v2 Reference

API **v2** is the second generation of the TalentUnveiled public API. It is reshaped natively around the **v3 interview engine** (competencies, qualifications, and eligibility) and adds full write access to jobs, applications, interview configuration, and webhooks.

<Note>
  API [v1](/api-reference/introduction) remains fully supported and unchanged. v2 is a separate, independent version — migrate when you are ready. The two share the same API keys.
</Note>

## Base URL

```
https://api.talentunveiled.com/api/v2
```

## Authentication

Authentication is identical to v1: include your API key in the `Authorization` header as a bearer token. A v2 key authenticates as the user who created it and can read and write that user's organization data.

```bash theme={null}
curl https://api.talentunveiled.com/api/v2/jobs \
  -H "Authorization: Bearer <your-api-key>"
```

Create and manage keys under **Developer Settings → API Keys** in the [HR portal](https://hr.talentunveiled.com) (see the [v1 guide](/api-reference/introduction#authentication) — the same keys work for v2).

<Warning>
  API keys are **personal**. A key belongs to the user who created it, and TalentUnveiled **revokes it immediately** when that user loses access to the organization (for example, when they off-board) — there is no grace period, and there are no service or shared-account keys. For a production integration, have a long-tenured teammate own the key and rotate it deliberately before they leave: create the new key, deploy it, verify it works, then revoke the old one.
</Warning>

## What's new in v2

* **v3-native interview results.** `GET /applications/{id}/interview` returns the v3 result: an `overall_score` (0–5, or `null` when not gradable), an `is_eligible` gate, and per-criterion `criteria_results`. Each criterion is a **competency** (scored 1–5 against a rubric) or a **qualification** (pass/fail). Read the `outcome` field first: `scored` means look at `score`/`passed`; `not_assessed` means there wasn't enough evidence — treat it as *unknown*, never as a failure.
* **Eligibility, not just score.** `is_eligible: false` means the candidate failed a *required* criterion. Surface it to recruiters — never auto-reject on it. `ineligibility_reasons` names exactly which required criteria failed.
* **Three independent scoring sub-resources** of an application: `/interview`, `/cv-evaluation`, and `/insights` — each with its own readiness `status`.
* **AI generation previews.** `POST /jobs/{id}/interview-config/criteria/generate` and `.../questions/generate` return AI-generated criteria/questions for review **without saving them**. Persist the (edited) result with the bulk `PUT`.
* **Idempotency.** Send an `Idempotency-Key` header on `POST` creates so retries don't double-create. A repeat with the same body replays the original response (`Idempotent-Replay: true`); a repeat with a different body returns `409`.
* **Webhooks.** Subscribe an endpoint to events and receive signed deliveries. See [Webhooks](/api-reference-v2/webhooks).

## Asynchronous results

An interview unfolds over minutes to days (scheduling, phone retries). Results are **eventually consistent**, so the scoring sub-resources (`/interview`, `/cv-evaluation`, `/insights`) never 404 while work is in progress.

**While no result exists yet,** the endpoint returns `200` with a small envelope — `{ "status": …, "reason": …, "result": null }`:

| `status`      | Meaning                                                                                                                                                                                 |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `processing`  | Not ready yet — keep polling (no faster than every 30s) or wait for the webhook.                                                                                                        |
| `unavailable` | No result will ever come. `reason` says why: `interview_not_available` (not a v3 interview, or the candidate opted out), `scoring_failed` (scoring permanently failed), or `no_result`. |

**Once the result is ready, the body is the full result object — not this envelope.** Its own `status` is the interview outcome and may be a terminal non-success (`completed`, but also `unreachable`, `failed`, `opted_out`, …). So: keep polling while the envelope `status` is `processing`; stop and treat it as no-result on `unavailable` (read `reason`); otherwise you have received the result object — branch on **its** `status`, and never wait for the envelope to report `"completed"` (it never does).

A `404` means the application itself is unknown (or belongs to another organization) — not "not ready yet". The recommended integration is **webhooks first, polling as a fallback**.

## Pagination

List endpoints use limit/offset pagination and always return a bounded page. `limit` defaults to **50** and is capped at **200** — a larger `limit` is silently clamped to 200, not rejected. Pass `?limit=N&offset=M` to page, and **follow `next` until it is `null`** (or page until you have `count` rows) to read the full set. Responses use the standard `{ count, next, previous, results }` envelope.

## Errors

Every error response is a JSON array of `{ code, detail, attr }` objects (`attr` names the offending field for validation errors, otherwise `null`):

```json theme={null}
[{ "code": "interview_version_mismatch", "detail": "Interview configuration is only available for v3 interview jobs.", "attr": null }]
```

## Endpoint reference

Browse every endpoint, with request and response schemas, in the **API v2** reference (generated from the OpenAPI specification). Scores are on a **1–5 scale** for criteria; the aggregate `overall_score` ranges **0–5** (or `null`).
