Sign in with Tabbio

Reading the live CV

The CV document, the PDF, the durable link that stays current, and ETag polling that costs almost nothing.

Most integrations treat a CV as a file somebody uploaded once. Tabbio treats it as a record the person keeps maintaining. Tabbio resolves the shared selection on every request, so when they fix a job title in the Tabbio app your next read has the fix. You never ask them to re-upload anything.

Everything on this page needs the cv:read scope.

The CV as JSON

GEThttps://server.tabbio.com/v1/me/cv
Terminal
curl https://server.tabbio.com/v1/me/cv \
  -H "Authorization: Bearer $TABBIO_ACCESS_TOKEN"
200 OK
{
  "data": {
    "id": "cv_7h2k9m3q",
    "title": "Main CV",
    "updatedAt": "2026-08-30T09:12:04.000Z",
    "isMainCv": true,
    "language": "en",
    "document": {
      "username": "sara",
      "publicCvUrl": "https://sara.tabbio.com/cv",
      "name": "Sara Ahmed",
      "title": "Product designer",
      "summary": "Product designer with eight years in fintech across the Gulf.",
      "bio": null,
      "location": "Dubai, United Arab Emirates",
      "avatarUrl": "https://cdn.tabbio.com/avatars/sara.jpg",
      "contactEmail": "sara@example.com",
      "isUaePassVerified": true,
      "socials": { "linkedin": "https://linkedin.com/in/sara", "phone": "+971 50 123 4567" },
      "openToWork": true,
      "skills": ["Design systems", "Prototyping", "User research"],
      "skillGroups": [{ "category": "Design", "items": ["Figma", "Design systems"] }],
      "languages": ["Arabic", "English"],
      "highlights": [{ "id": "hl_1", "title": "Cut onboarding drop off", "desc": "By 18 percent." }],
      "experiences": [
        {
          "id": "exp_1",
          "title": "Senior product designer",
          "company": "Careem",
          "location": "Dubai",
          "locationType": "Hybrid",
          "employmentType": "Full-time",
          "startDate": "2022-01",
          "endDate": null,
          "current": true,
          "overview": "Owned the rider experience.",
          "bullets": ["Rebuilt the rider onboarding flow."]
        }
      ],
      "educations": [
        {
          "id": "edu_1",
          "school": "American University of Sharjah",
          "degree": "BSc",
          "fieldOfStudy": "Design Management",
          "startDate": "2012-09",
          "endDate": "2016-06",
          "description": null
        }
      ],
      "certifications": [
        {
          "id": "cert_1",
          "name": "Nielsen Norman UX Certification",
          "issuer": "Nielsen Norman Group",
          "issueDate": "2023-04",
          "expirationDate": null,
          "credentialUrl": null
        }
      ],
      "projects": [
        {
          "id": "prj_1",
          "title": "Majlis design system",
          "description": "An Arabic first component library.",
          "image": null,
          "link": "https://github.com/sara/majlis",
          "technologies": ["React", "TypeScript"]
        }
      ]
    },
    "personalDetails": {
      "dateOfBirth": "1994-03-12",
      "gender": "Female",
      "maritalStatus": "Single",
      "nationality": "Emirati",
      "residence": "Dubai, United Arab Emirates",
      "visaResidency": "Citizen",
      "nationalService": null,
      "dependents": null
    },
    "publicProfileUrl": "https://sara.tabbio.com",
    "pdf": {
      "url": "https://server.tabbio.com/v1/cv-links/lnk_7h2k9m3q.9f1c0a8d4b2e6f37a15c8d0e2b4f6a91.pdf",
      "contentType": "application/pdf"
    }
  },
  "error": null,
  "meta": null
}

document is the canonical Tabbio CV document, the same shape the PDF renderer and the public profile consume. It is not a bespoke API projection, so what you render and what the person sees on their own profile agree.

isMainCv is true when the connection follows their main CV. If they pinned a specific CV on the consent screen instead, this is false and id stays fixed.

Two things that are easy to miss

languages is an array of plain strings, not objects. socials is an open map of network to value, so read socials.linkedin rather than searching a links array.

What each permission removes

The projection is applied server side, so what you receive is already masked. Fields are removed or nulled, never faked.

PermissionWhat you get without it
cv:contactpersonalDetails is null, and document.socials.phone is absent.
emaildocument.contactEmail is null, and email is absent from GET /v1/me.
Profile photodocument.avatarUrl and the user's avatarUrl are both null.
reading defensively
// personalDetails is null without cv:contact, and phone is simply absent from
// the socials map rather than present and empty.
const phone = cv.document.socials.phone ?? null;
const nationality = cv.personalDetails?.nationality ?? null;

Location, links, work history, education, skills, languages, certifications and projects are never private: a CV without those is not a CV.

Polling with an ETag

The response carries an ETag and Cache-Control: no-store. Send the tag back as If-None-Match and an unchanged CV answers 304 with no body, which is cheap enough to do on every page load.

poll.js
async function readCv(accessToken, etag) {
  const response = await fetch("https://server.tabbio.com/v1/me/cv", {
    headers: {
      Authorization: `Bearer ${accessToken}`,
      ...(etag ? { "If-None-Match": etag } : {}),
    },
  });

  if (response.status === 304) return { changed: false };

  const { data } = await response.json();
  return { changed: true, cv: data, etag: response.headers.get("ETag") };
}

Store the ETag next to your copy of the CV. The tag changes when the CV is edited, when the person switches which CV they share, and when they change the sharing options, so one comparison covers all three.

There are no webhooks yet

Nothing pushes you a cv.updated event today. Poll on a schedule that matches how fresh your product needs to be. Once a day is plenty for a candidate database; on demand, when somebody opens the profile, is better still.

The PDF

Two ways to get the same document, rendered by the same generator the person exports with in the app and masked by the same privacy projection as the JSON.

Stream it yourself

GEThttps://server.tabbio.com/v1/me/cv.pdf
js
const response = await fetch("https://server.tabbio.com/v1/me/cv.pdf", {
  headers: { Authorization: `Bearer ${accessToken}` },
});
const pdf = Buffer.from(await response.arrayBuffer());

Answers application/pdf. Use this when you need the bytes: to attach a CV to an email, or to archive one against an application.

GEThttps://server.tabbio.com/v1/cv-links/{token}

data.pdf.url on the JSON response is a link that needs no credential, because the signed token in the path is the credential. Its shape is <id>.<signature>.pdf. Put it in an employer-facing page, a Slack message, or an email, and it renders the current CV every time it is opened.

HTML
<a href="https://server.tabbio.com/v1/cv-links/lnk_7h2k9m3q.9f1c0a8d4b2e6f37a15c8d0e2b4f6a91.pdf">
  Open Sara's CV
</a>

Three things to know about it:

  • It is live, not a snapshot. An edit in Tabbio shows up the next time anyone opens the link.
  • It lives exactly as long as the consent behind it. Any of three things ends it: the person disconnects your app, they turn CV sharing off in Settings, Connected apps, or your own client revokes its refresh token. After any of those the link answers 404.
  • It is a secret. Anyone holding the URL can read the CV, and a tampered signature answers 404 rather than telling an attacker they were close. Do not log it, do not put it in a query string that ends up in an analytics pipeline, and do not expose it on a public page.

A dead link is not a dead connection

Read GET /v1/me/cv again and you get a fresh pdf.url. A 404 on a stored link means the consent behind that particular link ended, so re-read rather than asking the person to reconnect: if the token still works, the new link works too.

refresh-a-dead-link.js
async function currentPdfUrl(accessToken, storedUrl) {
  if (storedUrl) {
    const head = await fetch(storedUrl, { method: "HEAD" });
    if (head.ok) return storedUrl;
  }

  // Revoked, or never had one. A re-read mints a new link.
  const response = await fetch("https://server.tabbio.com/v1/me/cv", {
    headers: { Authorization: `Bearer ${accessToken}` },
  });
  if (!response.ok) return null;

  const { data } = await response.json();
  return data.pdf.url;
}

Snapshot when you need a record

A live link is right for "look at this candidate now". It is wrong for "this is the CV we hired against in March", because it will not say that any more. Store your own copy of document at the moment that matters.

When there is no CV

An account that has never built a CV answers 404 with CV_NOT_FOUND, even though the cv:read scope was granted. Treat it as an empty state rather than an error: the person connected successfully, they just have nothing to share yet.

Which CV am I reading

The person chooses on the consent screen and can change it afterwards in Settings, Connected apps. Your code does not choose and cannot ask for a different one.

GET /v1/me returns the current choice on connection.cv, so you can show it back to them:

JSON
{
  "connection": {
    "id": "con_9d21",
    "scopes": ["profile", "email", "cv:read"],
    "cv": { "id": "master", "title": "Main CV", "updatedAt": "2026-08-30T09:12:04.000Z" },
    "createdAt": "2026-08-30T09:10:41.000Z"
  }
}

connection.cv is null when cv:read was not granted. Telling somebody which CV your product is showing, with a line pointing at Tabbio Settings to change it, saves a support conversation later.