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
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.json3. 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
/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
| Field | Type | Required | Default | Constraints | Notes |
|---|---|---|---|---|---|
| matching_defaults | object | Yes | — | — | Org-level defaults: top_n, similarity_threshold, auto_filter_requirements, include_reasoning. |
| api_base_url | string | Yes | — | — | Base URL for all API calls (always https://vitae.build). |
| match_endpoint | string | Yes | — | — | Path to the match endpoint. Append to api_base_url. |
| rate_limits | object | Yes | — | — | requests_per_minute and daily_limit for the current tier. |
| usage | object | null | Yes | — | — | matches_today, daily_limit, remaining_today. Null for Free/Starter tiers. |
| tier | string | Yes | — | — | Current 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
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
/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
| Field | Type | Required | Default | Constraints | Notes |
|---|---|---|---|---|---|
| job_description | string | Yes | — | 20–50,000 chars | Full job description text. |
| top_n | integer | No | org default | 1–50 | Maximum candidates to return. |
| similarity_threshold | number | No | org default | 0.0–1.0 | Minimum cosine similarity to include. |
| include_reasoning | boolean | No | org default | — | Enable LLM re-ranking with reasoning text. |
| auto_filter_requirements | boolean | No | org default | — | Auto-extract hard requirements from JD. |
| filters | object | null | No | null | — | Hard filters: skills, languages, country, city. |
Default resolution order
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
| Field | Type | Required | Default | Constraints | Notes |
|---|---|---|---|---|---|
| matches | MatchCandidate[] | Yes | — | — | Ranked result list, may be empty. |
| total_candidates_searched | integer | Yes | — | — | Total candidates with embeddings in the org. |
| total_above_threshold | integer | Yes | — | — | Candidates above threshold before top_n cut. |
| embedding_model | string | Yes | — | — | Model used for similarity scoring. |
Match candidate fields
MatchCandidate
| Field | Type | Required | Default | Constraints | Notes |
|---|---|---|---|---|---|
| candidate_id | UUID | Yes | — | — | Unique candidate identifier. |
| first_name | string | Yes | — | — | Candidate first name. |
| last_name | string | Yes | — | — | Candidate last name. |
| professional_title | string | null | Yes | — | — | Current job title. |
| current_company | string | null | Yes | — | — | Current employer. |
| skills | string[] | Yes | — | — | Extracted skill list. |
| candidate_url | string | Yes | — | — | Deep link to candidate in the Vitae UI. |
| similarity_score | number | Yes | — | 0.0–1.0 | Embedding cosine similarity (always present). |
| match_score | number | null | Yes | — | 0.0–1.0 | LLM re-ranking score. Null if reasoning disabled. |
| reasoning | string | null | Yes | — | — | LLM explanation of match quality. Null if reasoning disabled. |
| profile_links | ProfileLink[] | Yes | — | — | Deep 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:
- Fetch org defaults — call
GET /api/v1/org/settings/matching/skill-config. Checkusage.remaining_today. If 0, tell the user the daily limit is reached and stop. - 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.
- 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).
- Call the match endpoint —
POST /api/v1/candidates/matchwith the job description, parameters, and any filters. - Present results — for each candidate, show name, title, company, key skills, score, and reasoning. Include
candidate_urllinks. Group by match quality if helpful (strong / partial / weak). - 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:
- Embedding similarity — the job description is embedded using
text-embedding-3-smalland compared against candidate embeddings via pgvector cosine similarity. This producessimilarity_score. - LLM re-ranking — when
include_reasoning: true, the top candidates are re-ranked by GPT with a detailed reasoning explanation. This producesmatch_scoreandreasoning.
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 skillslanguages(string[]) — match candidates who speak at least one of these languagescountry(string) — match candidates in this countrycity(string) — match candidates in this city
Filters narrow, threshold widens
Error handling
Error matrix
| Status | Likely cause | How to fix | Retry? |
|---|---|---|---|
| 200 | Success — matches returned (may be empty) | Parse and present results | no |
| 401 | Missing, expired, or invalid API key | Check ~/.vitae/config.json and verify the key in Settings → API Keys | no |
| 403 | Free or Starter tier (matching requires Professional+) | Upgrade at Settings → Billing | no |
| 422 | Validation error (short JD, out-of-range params, unknown fields) | Check job_description length (≥20 chars), parameter ranges, remove unknown fields | no |
| 429 | Rate limit (10/min) or daily quota exceeded | Wait for rate limit reset, or try tomorrow for daily quota (resets midnight UTC) | depends |
| 500 | Server error | Retry once with backoff, then report the error | yes |
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: falsefor 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
Related pages
- Candidate Matching Overview
- Matching Request & Response
- Matching Errors & Limits
- Authentication & API Keys