Developer Docs

AI Agent Skill Integration

Give AI agents access to Vitae candidate matching. Configure API keys, add agent instructions, and automate talent shortlisting.

This guide explains how to give AI agents access to Vitae's candidate matching pipeline. Whether you use Cursor, Claude Projects, ChatGPT, Cline, or any other agent framework, the pattern is the same: store an API key locally, give the agent instructions for calling two endpoints, and let it search your talent pool by job description.

Two endpoints, one workflow

Agents should always call the config endpoint first (free, no quota cost) to get org defaults and check remaining quota, then call the match endpoint with those defaults.

Setup

1. Create an API key

Go to Settings → API Keys → New Key in the Vitae UI. Copy the key — it starts with vtk_ and is only shown once.

2. Store the key locally

Agents that use shell commands (curl, scripts) read the key from a local config file. Create it with secure permissions:

mkdir -p ~/.vitae
echo '{"api_key": "vtk_YOUR_KEY_HERE"}' > ~/.vitae/config.json
chmod 600 ~/.vitae/config.json

3. Add instructions to your agent

Copy the agent instructions from the AI Settings → Agent Skills card and paste them into your agent's context. Where you paste depends on your framework:

  • Cursor.cursor/rules/ or project-level rules
  • Claude Projects — Project instructions panel
  • ChatGPT — Custom instructions or project knowledge
  • Cline / Windsurf — Project-level instructions file

4. Verify

Test that the key works by calling the config endpoint. This is free and doesn't count against your daily quota:

curl -s "https://vitae.build/api/v1/org/settings/matching/skill-config" \
  -H "Authorization: Bearer $(jq -r .api_key ~/.vitae/config.json)" | jq .

A 200 response with your org's matching defaults, tier, and usage means everything is working.

Config endpoint

Endpoint contract

GET/api/v1/org/settings/matching/skill-config
Authentication
Bearer vtk_ API key
Plan requirement
Any authenticated user
Rate limits
No quota cost

Returns the organisation's matching defaults, rate limits, and current daily usage. Agents should call this before every match session to get fresh defaults and check remaining quota.

SkillConfigResponse

FieldTypeRequiredDefaultConstraintsNotes
matching_defaultsobjectYesOrg-level defaults: top_n, similarity_threshold, auto_filter_requirements, include_reasoning.
api_base_urlstringYesBase URL for all API calls (always https://vitae.build).
match_endpointstringYesPath to the match endpoint. Append to api_base_url.
rate_limitsobjectYesrequests_per_minute and daily_limit for the current tier.
usageobject | nullYesmatches_today, daily_limit, remaining_today. Null for Free/Starter tiers.
tierstringYesCurrent subscription tier: free, starter, professional, enterprise.
{
  "matching_defaults": {
    "top_n": 10,
    "similarity_threshold": 0.5,
    "auto_filter_requirements": true,
    "include_reasoning": true
  },
  "api_base_url": "https://vitae.build",
  "match_endpoint": "/api/v1/candidates/match",
  "rate_limits": {
    "requests_per_minute": 10,
    "daily_limit": 30
  },
  "usage": {
    "matches_today": 5,
    "daily_limit": 30,
    "remaining_today": 25
  },
  "tier": "professional"
}

Check remaining quota first

If usage.remaining_today is 0, inform the user that the daily limit has been reached instead of calling the match endpoint. Daily limits reset at midnight UTC.

Match endpoint

Endpoint contract

POST/api/v1/candidates/match
Authentication
Bearer JWT or Bearer vtk_ API key
Plan requirement
Professional+
Rate limits
10/min, Pro 30/day, Enterprise 500/day

Runs a two-stage search against the organisation's candidate pool: pgvector embedding similarity pre-filter, then optional LLM re-ranking with reasoning. Each call counts against the daily quota.

Request schema

MatchRequest

FieldTypeRequiredDefaultConstraintsNotes
job_descriptionstringYes20–50,000 charsFull job description text.
top_nintegerNoorg default1–50Maximum candidates to return.
similarity_thresholdnumberNoorg default0.0–1.0Minimum cosine similarity to include.
include_reasoningbooleanNoorg defaultEnable LLM re-ranking with reasoning text.
auto_filter_requirementsbooleanNoorg defaultAuto-extract hard requirements from JD.
filtersobject | nullNonullHard filters: skills, languages, country, city.

Default resolution order

When a parameter is omitted, the API applies the org-level default (from the config endpoint). If no org default exists, system defaults apply: top_n=10, threshold=0.5, reasoning=true, auto_filter=true.

Request examples

curl -s -X POST "https://vitae.build/api/v1/candidates/match" \
  -H "Authorization: Bearer $(jq -r .api_key ~/.vitae/config.json)" \
  -H "Content-Type: application/json" \
  -d '{
    "job_description": "Senior Python Developer with FastAPI and PostgreSQL experience for a SaaS product team.",
    "top_n": 5
  }' | jq .

Response schema

MatchResponse

FieldTypeRequiredDefaultConstraintsNotes
matchesMatchCandidate[]YesRanked result list, may be empty.
total_candidates_searchedintegerYesTotal candidates with embeddings in the org.
total_above_thresholdintegerYesCandidates above threshold before top_n cut.
embedding_modelstringYesModel used for similarity scoring.

Match candidate fields

MatchCandidate

FieldTypeRequiredDefaultConstraintsNotes
candidate_idUUIDYesUnique candidate identifier.
first_namestringYesCandidate first name.
last_namestringYesCandidate last name.
professional_titlestring | nullYesCurrent job title.
current_companystring | nullYesCurrent employer.
skillsstring[]YesExtracted skill list.
candidate_urlstringYesDeep link to candidate in the Vitae UI.
similarity_scorenumberYes0.0–1.0Embedding cosine similarity (always present).
match_scorenumber | nullYes0.0–1.0LLM re-ranking score. Null if reasoning disabled.
reasoningstring | nullYesLLM explanation of match quality. Null if reasoning disabled.
profile_linksProfileLink[]YesDeep links to specific CV profile variants.
{
  "matches": [
    {
      "candidate_id": "ab24c75d-1234-5678-9abc-def012345678",
      "first_name": "Jan",
      "last_name": "Janssens",
      "professional_title": "Senior Cloud Engineer",
      "current_company": "CloudCorp",
      "skills": ["AWS", "Terraform", "Python", "Kubernetes"],
      "candidate_url": "https://vitae.build/app/candidates/ab24c75d-...",
      "similarity_score": 0.87,
      "match_score": 0.92,
      "reasoning": "Strong match: 5+ years AWS experience, Terraform expertise...",
      "profile_names": ["Cloud Engineering", "DevOps"],
      "profile_links": [
        {
          "profile_id": "1111-aaaa-...",
          "profile_name": "Cloud Engineering",
          "profile_url": "https://vitae.build/app/candidates/ab24c75d-...?profile=1111-aaaa-..."
        }
      ]
    }
  ],
  "total_candidates_searched": 47,
  "total_above_threshold": 12,
  "embedding_model": "text-embedding-3-small"
}

Agent workflow

When a user asks an agent to find candidates (e.g., "Find me 5 senior Java developers in Brussels who speak Dutch"), the agent should follow this sequence:

  1. Fetch org defaults — call GET /api/v1/org/settings/matching/skill-config. Check usage.remaining_today. If 0, tell the user the daily limit is reached and stop.
  2. Build the job description — if the user pastes a full JD, use it as-is. If they describe the role informally, compose a structured JD from their input.
  3. Choose parameters — use org defaults from step 1 unless the user specifies different values (e.g., "find 5 candidates" overrides top_n, "only Dutch speakers" adds a language filter).
  4. Call the match endpointPOST /api/v1/candidates/match with the job description, parameters, and any filters.
  5. Present results — for each candidate, show name, title, company, key skills, score, and reasoning. Include candidate_url links. Group by match quality if helpful (strong / partial / weak).
  6. Offer next steps — suggest adjusting filters, lowering the threshold, viewing full profiles, or searching with different parameters.

Matching pipeline

The match endpoint uses a two-stage pipeline for fast, accurate results:

  1. Embedding similarity — the job description is embedded using text-embedding-3-small and compared against candidate embeddings via pgvector cosine similarity. This produces similarity_score.
  2. LLM re-ranking — when include_reasoning: true, the top candidates are re-ranked by GPT with a detailed reasoning explanation. This produces match_score and reasoning.

Results are sorted by match_score when reasoning is enabled, otherwise by similarity_score.

Understanding scores

  • similarity_score (0.0–1.0) — fast approximate match from pgvector. Always present. Good for quick filtering.
  • match_score (0.0–1.0) — nuanced LLM re-ranking score. Present when include_reasoning: true. More accurate but slower.

A similarity_score of 0.7+ usually indicates a strong semantic match. Below 0.4 is typically a weak match. The match_score accounts for nuances the embedding model misses (years of experience, specific certifications, cultural fit signals in the JD).

Filter options

Filters apply hard constraints before similarity scoring. Use them for non-negotiable requirements:

  • skills (string[]) — match candidates with at least one of these skills
  • languages (string[]) — match candidates who speak at least one of these languages
  • country (string) — match candidates in this country
  • city (string) — match candidates in this city

Filters narrow, threshold widens

Use filters for hard requirements (must speak Dutch, must be in Belgium) and adjust the similarity threshold for soft relevance. Start without filters to see your baseline, then add them to narrow down.

Error handling

Error matrix

StatusLikely causeHow to fixRetry?
200Success — matches returned (may be empty)Parse and present resultsno
401Missing, expired, or invalid API keyCheck ~/.vitae/config.json and verify the key in Settings → API Keysno
403Free or Starter tier (matching requires Professional+)Upgrade at Settings → Billingno
422Validation error (short JD, out-of-range params, unknown fields)Check job_description length (≥20 chars), parameter ranges, remove unknown fieldsno
429Rate limit (10/min) or daily quota exceededWait for rate limit reset, or try tomorrow for daily quota (resets midnight UTC)depends
500Server errorRetry once with backoff, then report the erroryes

Rate limits and quotas

  • Per-minute: 10 requests/minute
  • Daily (Professional): 30 matches/day
  • Daily (Enterprise): 500 matches/day

The config endpoint (GET /skill-config) does not count against quotas. Daily limits reset at midnight UTC.

Tips for better results

  • Longer JDs produce better matches. A full job description with responsibilities, requirements, and nice-to-haves produces much better semantic matches than "find me a Python developer."
  • Start with org defaults. The admin has tuned them for the org. Only override when the user explicitly asks.
  • Skip reasoning for speed. Set include_reasoning: false for faster results when you just need a quick list.
  • Sparse candidates won't appear. Candidates with less than 50 characters of text content have no embeddings and won't show up in results.
  • Matching is org-scoped. You only see candidates belonging to the organisation that owns the API key.

Integration checklist

  • Create a dedicated API key for the agent (Settings → API Keys)
  • Store the key in ~/.vitae/config.json with chmod 600
  • Always call skill-config before matching to check quota
  • Use org defaults from skill-config unless the user overrides
  • Handle 429 with backoff, inform user on daily limit
  • Always include candidate_url links in results
  • Add human review before external client communication
  • Candidate Matching Overview
  • Matching Request & Response
  • Matching Errors & Limits
  • Authentication & API Keys