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

# UI specification

> The screens you build in your product, what they contain, where things go, and which texts come from Vort.

You build the screens in your own product. This page fixes what those screens contain, where things go, and which texts come from Vort. It is what [certification](/certification) tests against. RFC 2119 keywords apply.

<Note>
  Wireframes are left-to-right for readability. In Hebrew (`he`) every screen MUST be mirrored right-to-left, including list order within rows, icons with a direction, and progress bars (which fill from the right).
</Note>

## What you may customize, and what you may not

| Customizable (yours) | Fixed (Vort's) |
| - | - |
| **Colors**: map your palette onto the roles below (background, surface, text, accent, info, warning, error/blocking, success, and the three verdict colors). | **Layout**: which elements exist on each screen, their order, and the slot each notice goes to. |
| **Fonts**: font families, and the glyph style of icons (you may use your own icon library for the icons named below, as long as each icon keeps its meaning). | **Content**: every notice `title`/`body`, validation message and suggestion, evidence text, price, credit amount and balance. Rendered verbatim from the payload. |
| | **Fixed labels** on this page (verdicts, requirement kinds, met states, reject reasons, the must cap). Use them exactly, in the recruiter's language. |
| | **Behavior**: progress while running, run id visible, price before reveal, decisions sent back, notices never hidden by default. |

Your product's own chrome around the Vort screens (navigation, page header, help menu) is yours. The rules above apply to everything inside it that shows Vort data.

Constraints on colors: text and icons MUST meet WCAG 2.1 AA contrast (4.5:1 for body text, 3:1 for large text and icons) on their background; `info`, `warning` and `blocking` MUST be distinguishable from each other and from normal content; meaning MUST never be carried by color alone (every state also has an icon or a label).

General layout rules:

* One number per fact. Do not add percentages, scores or counts Vort did not send.
* No nested cards: a candidate card does not contain other cards; notices inside a card are inline rows, not boxes within the box.
* Nothing Vort sent is hidden behind a click by default, with the exception explicitly allowed for long `evidence` text below.

## Fixed labels

| Value | English | Hebrew |
| - | - | - |
| verdict `strong` | Strong match | התאמה חזקה |
| verdict `good` | Good match | התאמה טובה |
| verdict `weak` | Weak match | התאמה חלשה |
| kind `must` | Must | חובה |
| kind `nice` | Nice to have | יתרון |
| met `true` | Met | מתקיים |
| met `false` | Not met | לא מתקיים |
| met `null` | Unknown | לא ידוע |
| must cap | Up to 3 must requirements | אפשר לסמן עד 3 דרישות חובה |
| run id label | Run ID | מזהה ריצה |
| reject reasons | see [Reject reasons](/integration-flow#reject-reasons) | same table |

Your own UI chrome (button captions such as "Find candidates", "Reveal", "Save") is yours to word, provided it does not contradict Vort's content.

## Screen 1: Connect Vort

Where: your customer-admin settings. Purpose: redeem the activation code.

```text Wireframe theme={null}
┌─ Integrations › Vort ─────────────────────────────────────────────┐
│                                                                   │
│  Connect your Vort account                                        │
│  Paste the activation code you received from Vort.                │
│                                                                   │
│  Activation code  [ VORT-____-____            ]   ( Connect )     │
│                                                                   │
│  ⚠ <message from the API, e.g. code_expired>                     │  ← only on error
└───────────────────────────────────────────────────────────────────┘

After success:

┌─ Integrations › Vort ─────────────────────────────────────────────┐
│  ✓ Connected to Vort organization:  <organization_name>           │
│    Connected on 2026-10-01 by dana@acme-staffing.example          │
└───────────────────────────────────────────────────────────────────┘
```

* The input is posted to **your** server, which calls `POST /activations/redeem`. The browser never talks to Vort.
* On `404 code_not_found`, `409 code_already_redeemed`, `410 code_expired`, show the API `message` under the input. On `409 external_id_taken`, tell the admin this workspace is already connected and to contact Vort.
* Reconnect: on `401 invalid_org_token` from any later call, show this screen again in the disconnected state with a note that the connection must be renewed.

<Warning>
  The org token MUST NOT be displayed, logged to the browser console, stored in browser storage, or returned to the browser in any response. The connected state shows `organization_name` only.
</Warning>

## Screen 2: Job and requirements editor

Where: your job page. Purpose: send the job and the recruiter's requirements, surface the must cap before submit, and show Vort's validation.

```text Wireframe theme={null}
┌─ Senior Backend Engineer · Tel Aviv ──────────────────────────────┐
│                                                                   │
│  Requirements                          Must: 2 / 3                │
│  ┌─────────────────────────────────────────────────────────────┐  │
│  │ 1  5+ years of backend development in Go or Java  [Must ▾]  │  │
│  │    ⓘ "5+ years" sets no upper limit. If you meant a range…  │  │ ← warning (from API)
│  │ 2  Production experience with Kafka               [Must ▾]  │  │
│  │ 3  Payments or fintech domain                     [Nice ▾]  │  │
│  │ 4  Lives in Haifa                                 [Nice ▾]  │  │
│  │    ✖ A location cannot be a requirement. …                  │  │ ← rejected (from API)
│  │      Suggestion: Put the location in the job's location.    │  │
│  │ +  Add requirement                                          │  │
│  └─────────────────────────────────────────────────────────────┘  │
│                                                                   │
│  Candidates to find  [ 30 ]                                       │
│                                                ( Find candidates )│
└───────────────────────────────────────────────────────────────────┘
```

* Each requirement has its text and a `must`/`nice` control, in the order the recruiter arranged them. That order is the `index` Vort uses everywhere.
* **Must cap before submit.** A counter `Must: n / 3` is always visible above the list. When 3 musts are set, the control that would make a 4th must is disabled and shows the fixed must-cap label. The request MUST NOT be sent with more than 3 musts; if it is (for example, an old client), the `422 too_many_musts` message is shown like any other rejection.
* Job fields (`title`, `description_text`, `location_text`) come from your job record. On `422 job_rejected`, show each `rejected[]` message next to its field.
* **Rejected (`422 requirements_rejected`)**: nothing was started. Show every `rejected[]` item's message, and its suggestion when not null, directly under the requirement at `index`, verbatim, in the error color with the ✖ icon. Keep the recruiter's text in the field so it can be edited, and keep "Find candidates" available for the resubmit.
* **Warnings** (in a `422` body or in the `202` response): show each under its requirement with the ⓘ icon in the warning color. They MUST NOT block submission. After a `202`, the warnings stay visible on the results screen's requirement list for that run.
* If the recruiter submits no requirements, omit `requirements`; Vort derives them from the job description.
* "Find candidates" calls your server, which calls `POST /runs`. On a timeout or `5xx`, do not auto-retry (it would start a second run); show an error with a manual retry.

## Screen 3: Run results

Where: opens right after `202` with the `run_id`. Purpose: progress, notices, candidates.

```text Wireframe theme={null}
┌─ Senior Backend Engineer · Tel Aviv ──────────────────────────────┐
│ Run ID 9f2b6c1e-3d4a-4e8f-b1c7-0a5d2e9f6b33 [copy]                 │ ← always visible
│                                                                   │
│ Searching…  ███████░░░░░░░░░░░░░░░  7 / 30 confirmed · 184 screened│ ← while running
│                                                                   │
│ ┌ above_list ───────────────────────────────────────────────────┐ │
│ │ ⚠ Search widened beyond Tel Aviv                              │ │ warning
│ │   Not enough candidates were found in Tel Aviv, so candidates │ │
│ │   from outside the area are shown as well.                    │ │
│ │ ⓘ <title>                                                     │ │ info
│ │   <body>                                                      │ │
│ └───────────────────────────────────────────────────────────────┘ │
│                                                                   │
│ Requirements                                                      │
│  0 Must  5+ years of backend development in Go or Java            │
│  1 Must  Production experience with Kafka                         │
│          ⓘ 41 candidates were rejected because of this requirement│ ← on_requirement
│  2 Nice  Payments or fintech domain                               │
│                                                                   │
│ ┌ candidate card ───────────────────────────────────────────────┐ │
│ │ …see Screen 4…                                                │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ ┌ candidate card ───────────────────────────────────────────────┐ │
│ └───────────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────┘
```

Empty list:

```text Wireframe theme={null}
│ ┌ empty_state ──────────────────────────────────────────────────┐ │
│ │ ⛔ <title>                                                    │ │ blocking / warning / info
│ │    <body>                                                     │ │
│ └───────────────────────────────────────────────────────────────┘ │
```

### Run ID

<Info>
  The `run_id` MUST be visible on this screen without interaction (in the header or a footer line of the results area), labeled with the fixed "Run ID" label, selectable, with a copy action. You MAY show it in a smaller, secondary style. It MUST also appear in any error message you show for this run.
</Info>

### Progress

* While `status` is `running`, show **`progress.confirmed / progress.target`** as a number pair and a determinate bar. A spinner alone is not enough. You MAY add `progress.screened` as secondary text.
* Update on every poll (recommended every 5 s while the screen is open). New candidates are appended in API order; do not reorder cards the recruiter is looking at, except to insert new ones where the API places them.
* On `completed`, replace "Searching…" with the final count (`confirmed / target`). If `confirmed < target`, the notices explain why; do not add your own explanation.
* On `failed`, stop the progress indicator and show the notices (typically in `empty_state` or `above_list`) and the `run_id`.
* The recruiter MAY leave the screen while a run is running. The run continues on Vort's side; returning to the screen resumes polling.

### Notices

Generic-rendering rule and catalogue: [Notices](/notices).

| Slot | Placement |
| - | - |
| `above_list` | A band between the progress line and the requirement list. |
| `on_requirement` | An inline row directly under the requirement at `ref.requirement_index`. |
| `on_candidate` | An inline row inside the card of `ref.candidate_id`, below the header line. |
| `empty_state` | In place of the list when there are no candidates; in `above_list` otherwise. |

Per-severity visual rules:

| Severity | Icon | Color role | Rules |
| - | - | - | - |
| `info` | ⓘ | info | Title in medium weight, body in regular text. Always visible. |
| `warning` | ⚠ | warning | Left (RTL: right) accent bar in the warning color. Always visible; no dismiss control. Ordered before `info` notices in the same slot only if the API order already does so (keep API order). |
| `blocking` | ⛔ | error | Most prominent element in its slot. In `empty_state` it is the whole empty state. No dismiss control. If it sits above a non-empty list, the list stays visible below it. |

* Every notice shows `title` and `body` verbatim. If `body` is longer than 3 lines you MAY clamp it with a "more" control; `title` and the first line stay visible.
* Unknown `code`, unknown `slot`, unknown `severity`: see the fallbacks in [the generic-rendering rule](/notices#the-generic-rendering-rule).
* Report rendered codes in `notices_shown` (see [Screen 6](#screen-6-decision-capture)).

## Screen 4: Candidate card

```text Wireframe theme={null}
┌───────────────────────────────────────────────────────────────────┐
│ Yael Cohen                                     [ Strong match ]   │
│ Staff Backend Engineer · Northwind Payments · Ramat Gan, Israel   │
│ ⓘ <on_candidate notice title> — <body>                            │ ← only if any
│                                                                   │
│  ✓ Must  5+ years of backend development in Go or Java            │
│          Backend engineer since 2017; Go at Northwind (2021 to…)  │ ← evidence
│  ✓ Must  Production experience with Kafka                         │
│          Led migration of the ledger pipeline to Kafka.           │
│  ? Nice  Payments or fintech domain                               │
│          Unknown                                                  │ ← met null, evidence null
│                                                                   │
│ ( Shortlist )  ( Reject ▾ )  ( More ▾ )   ( Reveal contact · 15 credits ) │
└───────────────────────────────────────────────────────────────────┘
```

* Header: `name`, then `title · company · location` (omit null parts, no placeholders). The verdict badge uses the fixed label and the verdict color for `strong` / `good` / `weak`.
* One row per entry in `requirements[]`, in `index` order: met icon (✓ `true`, ✗ `false`, ? `null`), fixed kind label, requirement `text`, and `evidence` under it, verbatim. When `evidence` is null, show the fixed met label ("Unknown" / "Not met" / "Met") instead of an empty line.
* Evidence longer than 2 lines MAY be clamped with a "more" control. The met icon, kind and requirement text are never hidden.
* The card MUST NOT show a score, percentage or ranking number Vort did not send.
* Keep the API's candidate order.

## Screen 5: Reveal contact

```text Wireframe theme={null}
Before:                                  Confirm (optional step):
( Reveal contact · 15 credits )          ┌──────────────────────────────────┐
                                         │ Reveal contact for Yael Cohen?   │
                                         │ Price: 15 credits                │
                                         │        ( Cancel )  ( Reveal )    │
                                         └──────────────────────────────────┘
After:
┌───────────────────────────────────────────────────────────────────┐
│ ✉ yael.cohen@example.com        ☎ Not found                       │
│ 15 credits charged · Balance: 2,435 credits                        │
└───────────────────────────────────────────────────────────────────┘
```

<Warning>
  The price shown before the click MUST be the candidate's `reveal_price_credits` from the most recent `GET /runs/{run_id}` response. It MUST NOT be hard-coded, configured on your side, or reused from another run.
</Warning>

* After the reveal, show `email` and `phone` (a null value is shown as "not found", never as a blank), and `credits_charged` and `wallet_balance_after` exactly as returned.
* `contact_state: revealed` cards show the contact block instead of the reveal button (fetch the details again via reveal: a repeat reveal is not charged).
* On `402 insufficient_credits`, show the API `message`, `balance` and `required`. Do not offer a retry button that repeats the same call.

## Screen 6: Decision capture

Every decision below MUST be sent to `POST /runs/{run_id}/decisions`, with the acting recruiter, at the time it is made (or in a batch within minutes).

```text Wireframe theme={null}
( Shortlist )   ( Reject ▾ )                          ( More ▾ )
                ┌──────────────────────────────┐      ┌──────────────────┐
                │ Why rejected?                │      │ Contacted        │
                │ ○ CV not relevant            │      │ Interviewing     │
                │ ○ Overqualified              │      │ Hired            │
                │ ○ Distance / location        │      │ + optional note  │
                │ ○ Insufficient experience    │      └──────────────────┘
                │ ○ Candidate declined         │
                │ ○ Candidate withdrew         │
                │ ○ Already known to us        │
                │ Note (optional) [__________] │
                │                  ( Reject )  │
                └──────────────────────────────┘
```

* The reject menu MUST list exactly the seven fixed reasons with their fixed labels, plus an optional free-text note. Picking a reason is optional for the recruiter; if none is picked, send `reject_reason: null` and the note.
* Shortlisted, contacted, interviewing, hired are sent as those `decision` values. If your ATS pipeline has its own stages, map the stages that mean these five outcomes to them; every such transition for a Vort candidate MUST be sent, including transitions made later in your pipeline screens, not only on this screen.
* Notes travel in `note` on the decision they accompany, so offer the note field in the same step as the decision (as in the reject menu above). A note typed later is welcome: send the same decision again with the note and a NEW `occurred_at`, and Vort keeps it as a new entry in the candidate's history. Only an exact retry (same `occurred_at`) is deduplicated.
* Undo in your UI (for example "un-reject") does not delete the earlier decision at Vort in v1. Send the new decision (for example `shortlisted`); both are kept, with their `occurred_at`, so Vort reads the latest as the current state.

<Warning>
  `notices_shown`: include the codes of all notices rendered for this run (known and unknown). Send them with the first decision batch, and whenever a newly rendered code appears. If the recruiter leaves without deciding anything, send `decisions: []` with `notices_shown`.
</Warning>
