Open appvitae.ai

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.

TypeScope
jobsjobs_read
candidatescandidates_read
companiesclients_read
contactscontacts_read
applications, application_events, interviewsapplications_read
placementsplacements_read
callsactivities_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
fromRequired. Start of the window, inclusive. ISO 8601 with Z or an offset.
toRequired. End of the window, exclusive. At most 400 days after from.
limitRecords per page, 1 to 1,000. Default 500.
cursorThe 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
idThe record’s Vitae id.
typeThe record type.
owner_idThe Vitae user id it is credited to, or null. Matches users[].id.
created_at, updated_atISO 8601 UTC.
is_deletedtrue 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.

MetricCountDateCredited to
CVs sentapplications with presented_atpresented_atpresented_by_id
Interviews arrangedapplication_events with to_stage = interview-scheduledoccurred_atowner_id
1st interviews arrangedthe first such event per application_idoccurred_atowner_id
Interviews heldinterviews with scheduled_for, not cancelledscheduled_forowner_id
Offersapplication_events with to_stage = offer-extendedoccurred_atowner_id
Placementsplacements, not fell_throughcreated_atowner_id
Revenue bookedplacements.fee_amount in fee_currency, not fell_throughcreated_atowner_id
Startersplacements, not fell_throughstart_dateowner_id
Jobs addedjobscreated_atowner_id
Candidates addedcandidatescreated_atowner_id
Companies addedcompaniescreated_atowner_id
Contacts addedcontactscreated_atowner_id
Callscallsoccurred_atowner_id
BD callscalls with related_type contact or companyoccurred_atowner_id

Placements are credited to one person; Vitae does not split a placement between recruiters.