Partner API
Candidates
Read the people who applied to your jobs, their screening answers, the CV they submitted, and their resume file.
A candidate is one application to one job. Everything here needs candidates:read and an API key, and only reaches jobs at companies your app created.
List applications for a job
https://server.tabbio.com/v1/jobs/{jobId}/candidatesNewest first, page based. Each row already carries the candidate identity, their screening answers and a reference to the CV they submitted.
curl "https://server.tabbio.com/v1/jobs/$JOB_ID/candidates?status=applied&pageSize=100" \
-H "Authorization: Bearer $TABBIO_API_KEY"const url = new URL(`https://server.tabbio.com/v1/jobs/${jobId}/candidates`);
url.searchParams.set("status", "applied");
url.searchParams.set("pageSize", "100");
const response = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.TABBIO_API_KEY}` },
});
const { data: candidates, meta } = await response.json();import os
import requests
response = requests.get(
f"https://server.tabbio.com/v1/jobs/{job_id}/candidates",
headers={"Authorization": f"Bearer {os.environ['TABBIO_API_KEY']}"},
params={"status": "applied", "pageSize": 100},
)
response.raise_for_status()
candidates = response.json()["data"]| Field | Type | Description |
|---|---|---|
| status | string | Filter by application status. One of |
| query | string | Free text search over the candidate name, email and CV title. |
| page | integer | 1 based page number. Defaults to 1. |
| pageSize | integer | Items per page, 1 to 100. Defaults to 25. |
{
"data": [
{
"applicationId": "app_3t7u1v5w",
"jobId": "job_6p1q8r3s",
"companyId": "cmp_4k8m2n6p",
"status": "applied",
"appliedAt": "2026-09-02T07:41:00.000Z",
"updatedAt": "2026-09-02T07:41:00.000Z",
"source": "tabbio",
"screeningAnswers": [
{ "questionId": "q_visa", "type": "yes_no", "value": true }
],
"statusHistory": [{ "status": "applied", "at": "2026-09-02T07:41:00.000Z" }],
"candidate": {
"userId": "usr_8k2m",
"name": "Omar Haddad",
"username": "omar",
"image": "https://cdn.tabbio.com/avatars/omar.jpg",
"email": "omar@example.com",
"title": "Backend engineer"
},
"submittedCv": {
"id": "cv_1a2b",
"title": "Backend engineer CV",
"updatedAt": "2026-09-01T18:00:00.000Z"
}
}
],
"error": null,
"meta": { "page": 1, "pageSize": 100, "total": 1, "totalPages": 1 }
}The identity is candidate.userId, not applicationId: one person can apply to
several of your jobs, and userId is what joins those together.
submittedCv here is a reference. Read one application for the document itself.
Read one application
https://server.tabbio.com/v1/candidates/{applicationId}{
"data": {
"applicationId": "app_3t7u1v5w",
"jobId": "job_6p1q8r3s",
"companyId": "cmp_4k8m2n6p",
"status": "applied",
"appliedAt": "2026-09-02T07:41:00.000Z",
"updatedAt": "2026-09-02T07:41:00.000Z",
"source": "tabbio",
"screeningAnswers": [{ "questionId": "q_visa", "type": "yes_no", "value": true }],
"statusHistory": [{ "status": "applied", "at": "2026-09-02T07:41:00.000Z" }],
"candidate": {
"userId": "usr_8k2m",
"name": "Omar Haddad",
"username": "omar",
"image": "https://cdn.tabbio.com/avatars/omar.jpg",
"email": "omar@example.com",
"title": "Backend engineer"
},
"candidateProfile": {
"bio": "Backend engineer, payments and logistics.",
"about": null,
"skills": ["Go", "PostgreSQL", "Kubernetes"],
"interests": ["Distributed systems"]
},
"submittedCv": { "id": "cv_1a2b", "title": "Backend engineer CV", "updatedAt": "2026-09-01T18:00:00.000Z" },
"submittedCvDocument": {
"id": "cv_1a2b",
"title": "Backend engineer CV",
"summary": "Seven years building payment systems in the Gulf.",
"createdAt": "2026-05-02T10:00:00.000Z",
"updatedAt": "2026-09-01T18:00:00.000Z",
"snapshot": true
},
"additionalCvDocuments": [],
"coverLetter": "I have run cold chain logistics platforms for six years.",
"resume": {
"downloadUrl": "https://cdn.tabbio.com/resumes/app_3t7u1v5w.pdf?token=...",
"expiresAt": "2026-09-02T08:41:00.000Z",
"filename": "omar-haddad-cv.pdf"
}
},
"error": null,
"meta": null
}What each part is
| Field | Type | Description |
|---|---|---|
| candidate | object | Identity: userId, name, username, image, email, title. |
| candidateProfile | object | Their live Tabbio profile: bio, about, skills, interests. This follows their edits. |
| submittedCvDocument | object or null | The CV they attached to this application. snapshot true means it was frozen at apply time. |
| additionalCvDocuments | object[] | Other CVs on their account, when they chose to share more than one. |
| coverLetter | string or null | Plain text, when they wrote one. |
| resume | object or null | A short lived link to an uploaded resume file, with its original filename. |
| screeningAnswers | object[] | One entry per question, keyed by questionId, with the value typed as the question was. |
| statusHistory | object[] | Every status this application has been in, with timestamps. |
Snapshot, not live
submittedCvDocument.snapshot tells you whether the CV was frozen when they applied.
A snapshot never changes, which is what makes it safe to keep in your own records: it
is the document the hiring decision was actually made against. candidateProfile is
the opposite, and follows their edits.
The resume link expires
resume.expiresAt is usually an hour out. Fetch the file when you need it rather than
storing the URL.
async function downloadResume(applicationId) {
const response = await fetch(
`https://server.tabbio.com/v1/candidates/${applicationId}`,
{ headers: { Authorization: `Bearer ${process.env.TABBIO_API_KEY}` } },
);
const { data } = await response.json();
if (!data.resume) return null;
const file = await fetch(data.resume.downloadUrl);
return {
filename: data.resume.filename ?? `${applicationId}.pdf`,
bytes: Buffer.from(await file.arrayBuffer()),
};
}Cache the bytes if you need them. Do not cache downloadUrl.
Matching answers to questions
screeningAnswers[].questionId refers to screeningQuestions[].id on the job, so read
the job once and build a map.
const { data: job } = await getJob(jobId);
const prompts = new Map(job.screeningQuestions.map((q) => [q.id, q.prompt]));
for (const answer of application.screeningAnswers) {
console.log(prompts.get(answer.questionId) ?? answer.questionId, "=>", answer.value);
}value is a string, a number or a boolean, matching the question's type.
Polling for new applications
There are no webhooks yet, so poll. Keep the newest appliedAt you have seen per job
and stop paging once you reach it.
async function newCandidates(jobId, since) {
const found = [];
let page = 1;
let totalPages = 1;
do {
const url = new URL(`https://server.tabbio.com/v1/jobs/${jobId}/candidates`);
url.searchParams.set("page", String(page));
url.searchParams.set("pageSize", "100");
const response = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.TABBIO_API_KEY}` },
});
const { data, meta } = await response.json();
for (const candidate of data) {
// The list is newest first, so the first old one ends the walk.
if (since && candidate.appliedAt <= since) return found;
found.push(candidate);
}
totalPages = meta.totalPages;
page += 1;
} while (page <= totalPages);
return found;
}Every five minutes across your live jobs sits comfortably inside the rate limits. See Errors and rate limits.
A cheaper check first: applicationCount on the job tells you whether anything is new
without paging the applications at all.
What you cannot do
- Change an application's status. There is no
candidates:writescope in this version. Move people through your own pipeline; Tabbio holds the application as submitted, andstatusHistoryshows what happened inside Tabbio. - Message a candidate. Use
candidate.email, and say who you are and which job you are writing about. - See applications for a job you did not create. That answers 403 with
COMPANY_NOT_MANAGED_BY_APP, and an unknown id answers 404.
Handling personal data
Applications are personal data about a real person in a real job market.
- Take only what your product uses. Storing the whole document when you render three fields is a liability, not a feature.
- Keep it as long as the hiring process needs and no longer.
- Let a candidate's data be deleted when they ask, in your systems as well as in Tabbio.
- Never pass this to a third party the candidate did not agree to. They applied to an employer, not to your data pipeline.