Reporting Sync API
The reporting sync gives a reporting or performance tool (OneUp, a BI warehouse, a spreadsheet job) a read-only copy of a workspace’s recruitment activity. One endpoint per record type lists every record created or modified between two date-times, a page at a time, with the user it is credited to, when it was created and whether it has been deleted. Poll it every few minutes for changes, or sync a year in one go.
Base URL: https://api.vitae.ai/v1/sync
Authentication
Use a workspace admin’s API key. In Settings > API keys, create a key with the
Reporting sync (read-only) option: it holds exactly the read scopes the sync needs
and cannot change anything. Present it as a bearer token, or in X-API-Key:
curl "https://api.vitae.ai/v1/sync/placements?from=2026-10-01T00:00:00Z&to=2026-11-01T00:00:00Z" \
-H "Authorization: Bearer vit_3f9c1e7a2b8d4f60_..."
Each type needs its own read scope; a key without it gets 403. The users and
deletions lists need any one of them.
| Type | Scope |
|---|---|
jobs | jobs_read |
candidates | candidates_read |
companies | clients_read |
contacts | contacts_read |
applications, application_events, interviews | applications_read |
placements | placements_read |
calls | activities_read |
Rate limit: 120 requests a minute per key, by default. Every response carries
X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; past the limit
you get 429 with Retry-After in seconds.
Listing changes
GET /v1/sync/{type}?from=<ISO 8601>&to=<ISO 8601>&limit=500&cursor=<next_cursor>
| Parameter | |
|---|---|
from | Required. Start of the window, inclusive. ISO 8601 with Z or an offset. |
to | Required. End of the window, exclusive. At most 400 days after from. |
limit | Records per page, 1 to 1,000. Default 500. |
cursor | The previous page’s next_cursor. Leave it out for the first page. |
A record is in the window when it was created or last modified inside it. Records come oldest change first. The response is plain JSON:
{
"type": "placements",
"records": [ { "id": "cm9x...", "owner_id": "cm7a...", "created_at": "2026-10-10T09:00:00.000Z", ... } ],
"next_cursor": "eyJ0IjoicGxhY2VtZW50cyIs...",
"has_more": true
}
Keep requesting with cursor=<next_cursor> until has_more is false. A cursor only
works for the type and window it came from; anything else answers 400. For an ongoing
sync, use windows that meet end to end (to of one call is from of the next) and
leave a minute or two behind the current time.
The users list also works without a window: it then returns every user.
One record
GET /v1/sync/{type}/{id}
Returns { "type": "...", "record": { ... } } with the same fields as the list, for
looking up a related record (the candidate behind a placement, for example). A deleted
record returns its tombstone; an id that never existed in the workspace answers 404.
Deleted records
- A record that is deleted but kept (jobs, candidates, companies, users) appears in its
list with
"is_deleted": true, in the window it was deleted in. - An archived application carries
archived_at; a placement that fell through carries"fell_through": true; a cancelled interview carries"cancelled": true. - A record removed outright appears in its list as a tombstone, in the window it was removed in:
{ "id": "cm9x...", "type": "contacts", "owner_id": null, "created_at": null,
"updated_at": "2026-10-12T14:03:11.000Z", "is_deleted": true, "deleted_at": "2026-10-12T14:03:11.000Z" }
GET /v1/sync/deletions?from&to lists every tombstone across types.
Fields
Every record has:
| Field | |
|---|---|
id | The record’s Vitae id. |
type | The record type. |
owner_id | The Vitae user id it is credited to, or null. Matches users[].id. |
created_at, updated_at | ISO 8601 UTC. |
is_deleted | true once deleted. |
No large text is included: no job adverts, CVs, email bodies or call notes.
users
Everyone who is or was a member of the workspace. id is the user id used by every
owner_id.
first_name, last_name, email, active (membership active), role, team_id,
team_name.
jobs
Owner: the job’s recruiter, else whoever created it.
title, status, job_type, category, client_id, client_name, location,
created_by_id, recruiter_id, salary_min, salary_max, salary_currency, archived.
candidates
Owner: the candidate’s owner, else recruiter, else creator.
name, source, status, client_id, created_by_id, location.
companies
Client companies. Owner: the account owner, else recruiter.
name, stage, deleted_at.
contacts
Client contacts. Owner: the contact owner, else recruiter.
name, job_title, status, client_id, client_name.
applications
A candidate on a job. Owner: the consultant who sent the CV.
candidate_id, job_id, client_id, stage, presented_at (when the CV was sent to
the client), presented_by_id, source, archived_at.
application_events
What happened to an application, one record per event; events never change.
Owner: who did it.
event (stage_changed or presented), application_id, candidate_id, job_id,
client_id, from_stage, to_stage, occurred_at.
Stages: new-candidate, interested, shortlisted, interview-scheduled,
interviewed, offer-extended, hired, rejected.
interviews
Interviews scheduled with a candidate. Owner: who scheduled it.
job_id, candidate_user_id, stage, format, scheduled_for, deadline,
duration_minutes, cancelled.
placements
Owner: the placing recruiter, else the consultant who sent the CV.
application_id, candidate_id, job_id, client_id, status, fell_through,
start_date, end_date, placed_by_id, presented_by_id, fee_amount,
fee_currency, fee_model, fee_status.
calls
Calls credited to a consultant: logged by hand, recorded by the Note taker, or imported.
Owner: the consultant. occurred_at (when the call happened), direction, outcome,
duration_seconds, related_type (candidate, contact or company), related_id,
client_id, title, source (logged, recorded or imported).
Recruitment metrics
These rules match Vitae’s own Leaderboard, so a reporting tool and Vitae count the same numbers.
| Metric | Count | Date | Credited to |
|---|---|---|---|
| CVs sent | applications with presented_at | presented_at | presented_by_id |
| Interviews arranged | application_events with to_stage = interview-scheduled | occurred_at | owner_id |
| 1st interviews arranged | the first such event per application_id | occurred_at | owner_id |
| Interviews held | interviews with scheduled_for, not cancelled | scheduled_for | owner_id |
| Offers | application_events with to_stage = offer-extended | occurred_at | owner_id |
| Placements | placements, not fell_through | created_at | owner_id |
| Revenue booked | placements.fee_amount in fee_currency, not fell_through | created_at | owner_id |
| Starters | placements, not fell_through | start_date | owner_id |
| Jobs added | jobs | created_at | owner_id |
| Candidates added | candidates | created_at | owner_id |
| Companies added | companies | created_at | owner_id |
| Contacts added | contacts | created_at | owner_id |
| Calls | calls | occurred_at | owner_id |
| BD calls | calls with related_type contact or company | occurred_at | owner_id |
Placements are credited to one person; Vitae does not split a placement between recruiters.