Conventions
- Base URL: written here as
{BASE_URL}. Every path is relative to{BASE_URL}/v1;POST /runsmeansPOST {BASE_URL}/v1/runs. The sandbox base URL is on the Introduction; the production base URL is issued at go-live. - Server-to-server only. Every request comes from your backend. A browser MUST NOT call this API and MUST NOT ever hold a
vpk_…key orvot_…token. - JSON only. Send
Content-Type: application/jsonwith a UTF-8 body on everyPOST. Responses areapplication/json; charset=utf-8. - Timestamps are ISO 8601 in UTC, for example
2026-10-04T09:12:44Z. - Ids.
run_id,job_id,user_id,organization_idare UUID strings.candidate_idis an integer (it can exceed 2^31; use a 64-bit type).external_idvalues are your own ids, as strings, unique per customer organization for users and jobs, and unique per partner for organizations. - Unknown fields. Every response and webhook body MAY gain new fields at any time. Your client MUST ignore fields it does not know. Do not use strict deserializers that fail on extra properties.
- Language. Notice
title/bodyand errormessagefollowx-vort-lang. Requirement and job validation issues always carry both_heand_entexts.
The 8 steps
1
Connect the customer
Vort issues the customer a one-time activation code (
VORT-XXXX-XXXX, valid 14 days by default). A customer admin pastes it into your “Connect Vort” screen. Your server redeems it with POST /activations/redeem and receives an org token (vot_…). Store it server-side, encrypted, keyed to that customer. It is shown to you once.Details: Authentication · Redeem activation code2
Register users
For each recruiter who will use Vort, call
POST /users with your own id for them (external_id). A user is a field on later calls, never a credential: recruiters never receive a Vort key or token.Details: Register a recruiter3
Create the job
POST /jobs with your job’s external_id, title, description (plain text, strip HTML) and location. Send it again whenever the job changes in your ATS: a repeat call updates title, description_text and location_text and returns the same job_id. Runs already started keep the description they started with. On 422 job_rejected, show each rejected[] message next to its field (title_missing, description_missing, location_missing).Details: Create or update a job4
Start a run
POST /runs with the job, the acting recruiter, a target count and, optionally, the recruiter’s requirements (each must or nice, at most 3 musts). You get 202 {run_id} immediately, or 422 with Vort’s messages if a requirement is certainly wrong. A cold run typically shows its first confirmed candidates within 10 to 17 seconds and keeps working for several minutes.See Requirement rules below. Details: Start a run5
Show progress and results
Poll
GET /runs/{run_id}. While the run is running, show progress.confirmed / progress.target. Render every entry in notices[] in its slot (see Notices). Show candidates with their verdict and per-requirement evidence. Show the run_id.See Run status below. Details: Get a run6
Reveal contact details
When a recruiter wants contact details, show the price from
reveal_price_credits before the click, then call POST /runs/{run_id}/candidates/{candidate_id}/reveal.See Reveal rules below. Details: Reveal contact details7
Report decisions
Every shortlist, rejection (with a reason from the fixed list), contact, interview, hire and note goes to
POST /runs/{run_id}/decisions, together with the notice codes the recruiter was shown (notices_shown).See Decisions and reject reasons below. Details: Report decisions8
Receive webhooks
Vort posts
run_completed, run_failed and candidate_revealed (one per revealed candidate) to your webhook URL, signed with x-vort-signature. Verify the signature on the raw body before parsing, and de-duplicate retries by event_id.Details: WebhooksRequirement rules
Requirements are validated before the run starts.kindismustornice. At most 3mustrequirements (too_many_musts). Surface this cap in your editor before submit (see Screen 2).- Vort rejects only what is certainly wrong and warns on what is probably wrong. It never silently changes or drops a requirement: the run uses your requirements in the same order and count, trimmed and with whitespace collapsed.
- When
requirementsis omitted, Vort derives the requirements from the job description. - Requirement indexes (
index) are 0-based positions in the array you sent. They are the same indexes used bycandidates[].requirements[].indexand by notices withref.requirement_index.
Show each rejected item’s message and, when not null, its suggestion, next to the requirement at
index, verbatim, and let the recruiter fix and resubmit. Warnings do not block the run; show them next to the requirement without blocking. Example payloads for every code are in Requirement validation examples.Run status
Treat any other
status value as running, but stop polling a run that has been running for more than 30 minutes and show the recruiter the run_id with a “contact support” hint.
object
Reveal rules
- Before calling reveal, your UI MUST have shown the recruiter the price from that candidate’s
reveal_price_creditsin the latestGET /runs/{run_id}response. Never hard-code or cache a price across runs; prices come from Vort’s price table at request time. emailandphonecan each benull: not every candidate has both. Render a missing value as “not found”, never as an empty field.- Show
credits_chargedandwallet_balance_afterafter the reveal. Do not assumecredits_chargedequals the quoted price; display what the response says. - After a reveal, the candidate’s
contact_statebecomesrevealedin laterGET /runs/{run_id}responses. A repeat reveal of the same candidate is not charged again. - On
402 insufficient_credits(withbalanceandrequired), showmessagewith both numbers and do not retry.
Decisions and reject reasons
Decision values:shortlisted, rejected, contacted, interviewing, hired.
Reject reasons
Only withdecision: "rejected". Your reject UI MUST offer exactly this list, with these labels.
- A rejection MAY have
reject_reason: nullonly when the recruiter did not pick a reason; put any free text innote. Never infer a reason code from free text. reject_reasonon any decision other thanrejectedis422 invalid_decision.- Free notes: a recruiter’s note travels in
noteon the decision it belongs to. v1 has no standalone note without a decision. occurred_atis when the recruiter acted in your UI, not when you sent it. You MAY batch decisions, but SHOULD send them promptly (Vort recommends within 5 minutes of the action).notices_shownlists the noticecodes rendered to a recruiter for this run, including codes you did not recognize. Send each code at least once per run; repeats are harmless. If the recruiter makes no decision, still senddecisions: []withnotices_shown.- The response is
{"accepted": n, "duplicates": n}. Treat any422as “nothing from this request was stored”: fix the batch and resend all of it. Items already stored come back asduplicates.
Request body
Objects
Candidate
Candidate
integer
The
candidate_id for reveal and decisions.string
Render as an opaque string (masked in the sandbox).
string | null
Current title.
string | null
Current employer.
string | null
Location.
string
strong | good | weakobject[]
One entry per run requirement.
string
locked | revealedinteger
Price of a reveal, in credits, at the time of this response.
Notice
Notice
string
info | warning | blockingstring
above_list | on_candidate | on_requirement | empty_statestring
Short heading. Render verbatim.
string
Full sentence(s). Render verbatim.
object
Optional.
requirement_index (for on_requirement) and/or candidate_id (for on_candidate).RequirementIssue
RequirementIssue
Used in
rejected[] and warnings[] of POST /runs.
integer
0-based position in the submitted
requirements array.string
A rejection or warning code (see Requirement rules). New codes may be added.
string
What is wrong. Show verbatim.
string | null
How to fix it. Show verbatim when not null.
JobIssue
JobIssue