Affinda Resume Parser Open console

The parse response

A successful parse returns { "data": ..., "meta": ... }. data is the resume. meta is what the parser can tell you about the document it just read. Those two keys are the only ones guaranteed to be there.

The response below is a real parse of a one-page resume, with most list entries and the two long text fields removed for length and the keys grouped for reading. Nothing in it was invented.

{
  "data": {
    "person": {
      "location": {
        "city": "Melbourne",
        "coordinates": {
          "latitude": -37.814,
          "longitude": 144.96332
        },
        "country": "Australia",
        "countryCode": "AU",
        "formatted": "Melbourne, Victoria, Australia",
        "raw": "Melbourne VIC 3000, Australia",
        "state": "Victoria"
      },
      "name": {
        "family": "Rivera",
        "given": "Alex"
      },
      "nationalities": []
    },
    "contact": {
      "emails": [
        "alex.rivera@example.com"
      ],
      "phoneNumbers": [
        {
          "callingCode": 61,
          "countryCode": "AU",
          "formatted": "+61 400 111 222",
          "nationalNumber": "0400 111 222",
          "raw": "+61 400 111 222"
        }
      ],
      "websites": [
        {
          "domain": "linkedin.com/in/alexrivera-example",
          "type": "linkedin",
          "url": "linkedin.com/in/alexrivera-example"
        }
      ]
    },
    "summary": "Backend engineer with eight years building document processing and search systems. Leads small teams, ships in Rust and Python, and cares about latency budgets.",
    "workExperience": [
      {
        "dateRange": {
          "durationMonths": 66,
          "end": {
            "current": true
          },
          "start": {
            "date": "2021-03-01",
            "precision": "month"
          }
        },
        "description": "Rebuilt the ingestion pipeline in Rust, cutting median parse latency from 1.8s to 260ms. Led a team of four engineers. Owned the on-call rotation for the extraction service.",
        "employmentType": "full_time",
        "jobTitle": "Senior Backend Engineer",
        "location": {
          "city": "Melbourne",
          "coordinates": {
            "latitude": -37.814,
            "longitude": 144.96332
          },
          "country": "Australia",
          "countryCode": "AU",
          "formatted": "Melbourne, Victoria, Australia",
          "raw": "Melbourne.",
          "state": "Victoria"
        },
        "occupation": {
          "managementLevel": "low",
          "normalizedTitle": "Engineer"
        },
        "organization": "Northwind Data"
      }
    ],
    "education": [
      {
        "dateRange": {
          "durationMonths": 48,
          "end": {
            "date": "2015-01-01",
            "precision": "year"
          },
          "start": {
            "date": "2012-01-01",
            "precision": "year"
          }
        },
        "fieldsOfStudy": [
          "Computer Science"
        ],
        "grade": {
          "metric": "Award"
        },
        "institution": "University of Melbourne",
        "level": "bachelor",
        "minors": [],
        "qualification": "Bachelor of"
      }
    ],
    "certifications": [
      {
        "date": {
          "date": "2022-01-01",
          "precision": "year"
        },
        "issuer": "Amazon Web Services",
        "name": "AWS Certified Solutions Architect, Associate."
      }
    ],
    "skills": [
      {
        "name": "Amazon Web Services",
        "sources": [
          {
            "section": "skills_interests_languages"
          }
        ],
        "taxonomy": {
          "affindaId": "af_e675p8fk2e",
          "isLanguage": false,
          "isSoftware": true,
          "type": "specialized_skill"
        },
        "text": "Amazon Web Services"
      },
      {
        "name": "Cloud-Native Computing",
        "sources": [
          {
            "section": "skills_interests_languages"
          }
        ],
        "taxonomy": {
          "affindaId": "af_bfz37z566r",
          "isLanguage": false,
          "isSoftware": false,
          "type": "specialized_skill"
        },
        "text": "Cloud Native Computing"
      }
    ],
    "languages": [
      {
        "name": "English"
      },
      {
        "name": "Spanish"
      }
    ],
    "projects": [],
    "employmentMetrics": {
      "averageTenureMonths": 42.3,
      "currentRoleTenureMonths": 66,
      "gaps": [],
      "longestTenureMonths": 66,
      "totalExperienceMonths": 127
    }
  },
  "meta": {
    "document": {
      "classification": {
        "confidence": 0.683,
        "label": "resume",
        "modelVersion": "document-domain-p1b-r2-clean-20260728"
      },
      "extractionQuality": {
        "band": "high",
        "score": 0.9384
      }
    },
    "schemaVersion": "1.0",
    "timing": {
      "totalMs": 52.557062
    }
  }
}

The rule that explains most of the shape

Schema v1 is sparse. A field the parser did not find is left out of the response, rather than returned as null or as an empty string. Objects that end up empty are dropped too. So a key that is missing means "not found", and you never have to tell null and absent apart.

Arrays are the exception: every list is always present, even when it is empty. In the example above, projects is [] because this resume has no projects section, not because the key was dropped.

Two consequences worth building in:

  • Read fields defensively. Use .get() in Python and optional chaining in TypeScript. This is true of nested keys too: person.name may hold given and family but no middle.
  • Do not treat an empty array as an error. A resume with no certifications returns certifications: [].

The set of key names is fixed. What varies between responses is which of them are present.

data: the resume

Key What it holds
person name (given, middle, family, title, suffix, preferred, postNominals), dateOfBirth, nationalities, and a parsed location.
contact emails, phoneNumbers, websites.
summary, objective The candidate's own summary and objective statements, as text.
workExperience One entry per role: jobTitle, organization, description, dateRange, location, employmentType, occupation.
education One entry per qualification: institution, qualification, level, fieldsOfStudy, minors, grade, dateRange, location.
certifications Certificates, courses and conferences, with name, issuer and date.
skills One entry per skill, with the matched taxonomy entry.
languages Languages the candidate claims.
projects, publications, patents, referees Their own sections when the resume has them.
achievements, associations, interests Lists of strings.
availability, workAuthorization, willingToRelocate, expectedSalary, preferredWorkLocations Stated preferences, when the resume states them.
employmentMetrics Totals derived from the work history: totalExperienceMonths, averageTenureMonths, longestTenureMonths, currentRoleTenureMonths, and gaps.
headshot The candidate's photo as base64, when one was found.
rawText The document's text, as extracted.
redactedText The same text with personal details masked.

Dates carry their own precision

A date is an object, not a string, and it says how precisely it was known:

{"date": "2021-03-01", "precision": "month"}

precision is year, month or day. A resume that says "March 2021" gives you a month precision date on the first of that month. Do not read 2021-03-01 as "the first of March".

A dateRange pairs a start and an end and adds durationMonths. A role the candidate is still in has "end": {"current": true} rather than an end date, as in the example above.

Skills carry a taxonomy match

Each skill has the text as it appeared in the resume, a normalized name, the sources it was found in, and a taxonomy block holding Affinda's identifier for that skill and whether it is a piece of software or a language. That is what lets you match "AWS" in one resume against "Amazon Web Services" in another.

Certifications are lifted out of education

An education entry whose level is a course, a certificate or a conference is re-emitted as a certifications entry. Read certifications for them rather than filtering education yourself.

Three fields are large

rawText, redactedText and headshot dominate the size of a response. They are returned by default, and the cloud endpoint takes no parameter that suppresses them, so if you are storing responses, drop the ones you do not use before you write them down.

meta: what the parser thought

{
  "meta": {
    "schemaVersion": "1.0",
    "document": {
      "classification": {
        "label": "resume",
        "confidence": 0.683,
        "modelVersion": "document-domain-p1b-r2-clean-20260728"
      },
      "extractionQuality": {"band": "high", "score": 0.9384}
    },
    "timing": {"totalMs": 52.557062}
  }
}
  • schemaVersion is the string 1.0. It is the version of this response format.
  • classification.label is resume, not_resume or uncertain, with a confidence between 0 and 1. This is the parser's opinion about whether the document was a resume at all. A batch of applications usually contains a few cover letters and portfolios, and this is how you find them.
  • extractionQuality.band is high, medium or low, with a score. It describes how cleanly text came out of the document, not how good the candidate is. A low band on a scanned document usually means a poor scan, and it is a good signal for routing something to a human.
  • timing.totalMs is the server-measured time for the request.

meta.ocr, when the document was scanned

A document with no text layer, such as a photograph or a scanned PDF, goes through OCR, and then meta carries a receipt of what OCR did:

{
  "ocr": {
    "routed": true,
    "sourcePageCount": 1,
    "processedPageCount": 1,
    "pageLimit": 3,
    "truncated": false,
    "pixelLimit": 2828000,
    "downscaledPageCount": 0,
    "downscaledAttemptCount": 0
  }
}

truncated is the field to watch. OCR reads the first three pages, so a five-page scan comes back with processedPageCount: 3 and truncated: true, and the fields on pages four and five are simply not there. Text documents are read further, with the first twenty pages contributing fields.

meta.ocr is absent on documents that had a text layer, which is most of them.

meta.document.warnings

warnings appears only when the parser continued past a quality gate that would otherwise have stopped it. Each entry has a code and a message, and the codes are degenerate_ocr, low_text_quality and ocr_truncated. Treat any of them as "this result deserves a look".

What is not in the response

There is no identifier for the parse and no URL to fetch it again. The API stores nothing, so the response you get is the only copy. If you need it later, write it down.

For the field-by-field contract, including every status code, see the API reference.