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.namemay holdgivenandfamilybut nomiddle. - 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}
}
}
schemaVersionis the string1.0. It is the version of this response format.classification.labelisresume,not_resumeoruncertain, with aconfidencebetween 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.bandishigh,mediumorlow, with ascore. 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.totalMsis 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.