Public Developer API
The public developer API lets server-to-server integrations read and safely write
core recruiting data with a scoped, organization-owned API key. It is a thin,
stable surface over the same recruiting graph the dashboard uses, with a curated
field contract that never exposes internal scoring, attribution, or free-form
Json blobs.
Base URL: https://api.vitae.ai/v1/public-api
Every public endpoint is mounted under /v1/public-api/<resource>. The v1 is
the global API version; public-api separates the key-authenticated developer
surface from the session-authenticated app API.
Authentication
Public endpoints are authenticated by an API key, not a user session. Present the key as a bearer token:
curl https://api.vitae.ai/v1/public-api/candidates \
-H "Authorization: Bearer vit_3f9c1e7a2b8d4f60_..."An X-API-Key header is also accepted for clients that reserve Authorization
for other purposes:
curl https://api.vitae.ai/v1/public-api/candidates \
-H "X-API-Key: vit_3f9c1e7a2b8d4f60_..."Keys have the shape vit_<prefix>_<secret>: a vit namespace, a 16-character
hex prefix used for fast lookup, and a high-entropy secret. Only the prefix and a
hash of the full key are ever stored.
Keys are minted from the dashboard (see Managing keys). The plaintext key is shown exactly once at creation — only a hash and a short prefix are stored — so capture it then. Each key is bound to one organization, and every request is implicitly scoped to that organization; there is no cross-tenant access and no organization id is ever taken from the request.
Scopes
A key carries an explicit set of scopes. A request to an endpoint whose scope the
key lacks is rejected with 403 Forbidden before any work is done. Scopes default
to none — a key with no scopes can authenticate but call nothing.
| Scope | Grants |
|---|---|
candidates_read | List and read candidates |
candidates_write | Create candidates |
jobs_read | List and read jobs |
jobs_write | Create jobs |
applications_read | List and read applications |
applications_write | Update an application’s stage and notes |
placements_read | List and read placements |
webhooks_read | List webhook subscriptions and delivery attempts |
webhooks_write | Create, update, and deactivate webhook subscriptions |
Grant the minimum scopes an integration needs. There is no placements_write
scope — placements are read-only over the public API.
Rate limiting
Every key has a per-minute request budget (default 120, configurable 1–10,000 at mint time). Each response carries the current budget:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed per minute for this key |
X-RateLimit-Remaining | Requests left in the current window |
Retry-After | Seconds to wait before retrying (only on 429) |
When the window is exhausted the API returns 429 Too Many Requests with a
Retry-After header. Back off for that many seconds rather than retrying
immediately.
Pagination
List endpoints use offset pagination with page (1-based, default 1) and limit
(default 25, max 100) query params:
GET /v1/public-api/candidates?page=2&limit=50The page metadata is returned under meta.pagination:
{
"data": [],
"meta": {
"pagination": {
"page": 2,
"limit": 50,
"total": 320,
"totalPages": 7,
"hasNextPage": true,
"hasPreviousPage": true,
"nextPage": 3,
"previousPage": 1
}
}
}Single-resource reads return a bare { "data": { ... } } envelope. Unknown query
params are rejected with 400 Bad Request.
Candidates
GET /v1/public-api/candidates candidates_read
GET /v1/public-api/candidates/:id candidates_read
POST /v1/public-api/candidates candidates_writeGET /candidates accepts an optional status filter alongside page/limit.
POST /candidates requires fullName; all other fields are optional. The owning
organization and the creating user are taken from the key — they cannot be set or
overridden by the client.
curl -X POST https://api.vitae.ai/v1/public-api/candidates \
-H "Authorization: Bearer vit_3f9c1e7a2b8d4f60_..." \
-H "Content-Type: application/json" \
-d '{
"fullName": "Ada Lovelace",
"email": "ada@example.com",
"skills": ["analytical-engines", "mathematics"]
}'The candidate projection includes: id, fullName, email, phoneNumber,
city, state, country, location, status, skills, minSalary,
maxSalary, salaryCurrency, bio, createdAt, updatedAt.
Jobs
GET /v1/public-api/jobs jobs_read
GET /v1/public-api/jobs/:id jobs_read
POST /v1/public-api/jobs jobs_writeGET /jobs accepts an optional status filter. POST /jobs requires subject;
the job is created with the platform’s safe defaults (active, public, draft
approval). The job projection includes the public posting fields: id, subject,
label, description, category, type, industry, role, country,
location, city, salaryMin, salaryMax, salaryCurrency, salaryFrequency,
experience, status, visibility, workplaceType, skills, createdAt,
updatedAt.
Applications
GET /v1/public-api/applications applications_read
GET /v1/public-api/applications/:id applications_read
PATCH /v1/public-api/applications/:id applications_writeGET /applications accepts optional jobId, candidateId, and stage filters.
Application creation is intentionally not exposed — it is entangled with the
internal matching pipeline. The only public write is a narrow PATCH of the
pipeline stage and/or free-form notes; at least one of the two must be
provided, or the request is rejected with 400.
curl -X PATCH https://api.vitae.ai/v1/public-api/applications/app_123 \
-H "Authorization: Bearer vit_3f9c1e7a2b8d4f60_..." \
-H "Content-Type: application/json" \
-d '{ "stage": "interview", "notes": "Strong systems background" }'The application projection includes: id, jobId, candidateId, clientId,
stage, type, source, notes, isMatched, approvalStatus, createdAt,
updatedAt.
Placements
GET /v1/public-api/placements placements_read
GET /v1/public-api/placements/:id placements_readPlacements are read-only. GET /placements accepts an optional status filter.
The placement projection includes: id, applicationId, candidateId, jobId,
clientId, status, startDate, endDate, closureReason, notes,
createdAt, updatedAt.
Errors
Public endpoints use the standard error envelope. Common statuses:
| Status | When |
|---|---|
400 Bad Request | Validation failed, unknown query param, or no writable field supplied |
401 Unauthorized | Missing, malformed, expired, or revoked key |
403 Forbidden | Key lacks the scope the endpoint requires |
404 Not Found | Resource does not exist in the key’s organization |
429 Too Many Requests | Per-key rate limit exceeded; see Retry-After |
Managing keys
API keys are created and rotated from the dashboard. These management endpoints are authenticated by a logged-in organization user (a session bearer token), not by an API key:
POST /v1/api-keys Mint a key (plaintext returned once)
GET /v1/api-keys List active keys (newest first)
POST /v1/api-keys/:id/rotate Rotate a key (new plaintext returned once)
DELETE /v1/api-keys/:id Revoke a key (irreversible)Rotate keys when a teammate or vendor loses access, and revoke any key you believe is compromised. Treat keys like passwords: never embed them in client-side code or commit them to source control.
Automation webhook subscriptions
Zapier and Make use API-key-authenticated webhook-subscription endpoints to create and remove REST-hook subscriptions without a dashboard session:
POST /v1/public-api/webhook-subscriptions webhooks_write
GET /v1/public-api/webhook-subscriptions webhooks_read
GET /v1/public-api/webhook-subscriptions/:id webhooks_read
PATCH /v1/public-api/webhook-subscriptions/:id webhooks_write
DELETE /v1/public-api/webhook-subscriptions/:id webhooks_write
GET /v1/public-api/webhook-subscriptions/:id/deliveries webhooks_readUse these endpoints for no-code platform hook lifecycle. The dashboard management
endpoints under /v1/webhook-subscriptions are still available for human-managed
subscriptions.
Security
- Always call over HTTPS.
- Scope keys to the minimum access an integration needs.
- Rotate on personnel or vendor changes; revoke on suspected compromise.
- Set an
expiresAton keys issued to short-lived integrations. - Resource ids are passed in the path, never PII — keep emails, phone numbers, and names out of URLs.