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

GEThttps://server.tabbio.com/v1/jobs/{jobId}/candidates

Newest first, page based. Each row already carries the candidate identity, their screening answers and a reference to the CV they submitted.

Terminal
curl "https://server.tabbio.com/v1/jobs/$JOB_ID/candidates?status=applied&pageSize=100" \
  -H "Authorization: Bearer $TABBIO_API_KEY"
FieldTypeDescription
statusstring

Filter by application status.

One of applied, reviewed, shortlisted, interview, offered, rejected, withdrawn

querystring

Free text search over the candidate name, email and CV title.

pageinteger

1 based page number. Defaults to 1.

pageSizeinteger

Items per page, 1 to 100. Defaults to 25.

200 OK
{
  "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

GEThttps://server.tabbio.com/v1/candidates/{applicationId}
200 OK
{
  "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

FieldTypeDescription
candidateobject

Identity: userId, name, username, image, email, title.

candidateProfileobject

Their live Tabbio profile: bio, about, skills, interests. This follows their edits.

submittedCvDocumentobject or null

The CV they attached to this application. snapshot true means it was frozen at apply time.

additionalCvDocumentsobject[]

Other CVs on their account, when they chose to share more than one.

coverLetterstring or null

Plain text, when they wrote one.

resumeobject or null

A short lived link to an uploaded resume file, with its original filename.

screeningAnswersobject[]

One entry per question, keyed by questionId, with the value typed as the question was.

statusHistoryobject[]

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.

resume.expiresAt is usually an hour out. Fetch the file when you need it rather than storing the URL.

fetch-resume.js
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.

answers.js
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.

poll-candidates.js
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:write scope in this version. Move people through your own pipeline; Tabbio holds the application as submitted, and statusHistory shows 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.