Skip to main content

Job Description Match API

The Job Description Match endpoint measures how closely a resume overlaps with the job description it was submitted for. It returns the share of the posting's keywords the resume uses, any passages the two share word for word, and a low / moderate / high overlap level.

Hiring teams often ask two separate questions about a resume: was it written by AI, and was it tailored to this job? The AI Detector API answers the first. This endpoint is a measure for the second, so you can call both and show the results side by side.

The result is a text overlap measurement. It says nothing about who wrote the resume, and tailoring a resume to a job is normal. High overlap can simply mean a good fit, so don't use this as the only basis for a hiring decision. See Interpreting the result.

tip

This endpoint is free of charge.

Sample Code​

curl -X POST https://api.sapling.ai/api/v1/jd-match \
-H "Content-Type: application/json" \
-d '{"key":"<api-key>", "text":"Software engineer with five years of Python and PostgreSQL experience. Built REST APIs on Kubernetes and led a team of three engineers. Designed and maintained scalable backend services for millions of users.", "job_description":"We are hiring a backend engineer to work on our payments platform. You have designed and maintained scalable backend services for millions of users. Strong Python and Go skills, PostgreSQL, Kubernetes, Terraform and observability tooling. You will mentor engineers and own payments reliability."}'

Request Parameters​

https://api.sapling.ai/api/v1/jd-match

HTTP method: POST

key: String
32-character API key. Can also be supplied via the Authorization header as a bearer token; if both are provided, the key parameter takes precedence.

text: String
The resume, as plain text. Up to 50,000 characters. It is compared exactly as sent (HTML isn't stripped), so the offsets in copied_spans line up with your own copy.

job_description: String
The job posting, as plain text. Up to 20,000 characters. It needs at least 5 distinct keywords to compare against.

Response Parameters​

The response for the cURL request above, with the keyword lists shortened:

{
"score": 0.786,
"level": "high",
"keyword_coverage": 0.571,
"matched_keywords": [
{"term": "backend", "job_description_count": 2, "resume_count": 1},
{"term": "python", "job_description_count": 1, "resume_count": 1}
// ...10 more
],
"missing_keywords": [
{"term": "payments", "job_description_count": 2},
{"term": "terraform", "job_description_count": 1}
// ...7 more
],
"copied_fraction": 0.312,
"has_copied_span": true,
"copied_spans": [
{
"start": 136,
"end": 207,
"text": "Designed and maintained scalable backend services for millions of users",
"words": 10
}
],
"copied_spans_total": 1,
"resume_words": 32,
"job_description_words": 43,
"version": "1.1.4"
}

score: Float
Overlap from 0 to 1. This is a heuristic, not a probability. See How the score is calculated.

level: String
low, moderate (score of at least 0.2) or high (score of at least 0.27).

keyword_coverage: Float
The share of the posting's keywords that appear in the resume, from 0 to 1.

matched_keywords: List[Object]
Keywords found in the resume: term (lowercase), job_description_count and resume_count.

missing_keywords: List[Object]
Keywords not found in the resume: term (lowercase) and job_description_count.

copied_fraction: Float
The share of the resume's words that are inside a copied span, from 0 to 1.

has_copied_span: Boolean
Whether the resume shares any run of 6 or more words verbatim with the posting.

copied_spans: List[Object]
The copied passages in document order, at most 50: start and end (character offsets into text, end exclusive), text (the passage as it appears in the resume) and words.

copied_spans_total: Integer
The number of copied passages, which can be more than the 50 listed.

resume_words, job_description_words: Integer
Word counts of the two inputs.

version: String
The version of the matching algorithm.

How the score is calculated​

score = 0.5 * keyword_coverage + 0.5 * min(1, copied_fraction / 0.25)

Half of the score comes from keyword coverage. The other half comes from copying, and reaches its maximum once a quarter of the resume's words are in copied spans.

Keywords are the posting's words and repeated two-word phrases, after removing common English words and job posting boilerplate such as "experience" and "years". Matching ignores case, and technical terms such as C++, C#, Node.js and CI/CD stay whole.

The same inputs give the same result. No AI model is involved, so responses are fast.

Interpreting the result​

  • Overlap is not authorship. Combine this with the AI Detector API if you need both signals. Neither one should decide an application by itself.
  • Overlap is not misconduct. A qualified candidate's resume naturally shares a posting's vocabulary, and many candidates tailor their resumes. Use the result to prompt a closer read.
  • The levels are provisional. The thresholds were set using simulated tailored resumes, not real applications, so real tailoring may score lower, and they may change as we measure more. version identifies the algorithm that produced a result.
  • A copied span is the most specific signal, but check it. Resumes not tailored to the posting rarely share a run of 6 or more words with it, although a stock phrase such as "excellent written and verbal communication skills" can trigger it. Read copied_spans before relying on has_copied_span.
  • Keywords can be noisy. Company names and words that appear only once in the posting can count as keywords, so an entry in missing_keywords doesn't always point to a missing skill.
  • It is tuned for English. Other languages are accepted, but the common-word and boilerplate lists are English, so expect noisier keywords.

Errors​

StatusCause
400text or job_description is missing, empty, not a string, over its length limit or contains a NUL character, or the posting has fewer than 5 distinct keywords.
401The API key is missing or invalid.
402The account can't make API requests, for example because its quota is used up.
429Rate limited. Both texts count against your key's rate limit.

The 400 and 401 responses have a msg field with the reason.

Notes​

  • Offsets count Unicode code points (Python str indexing), not UTF-16 code units or bytes. In JavaScript, use Array.from(text) if the input may contain characters outside the Basic Multilingual Plane.
  • This endpoint doesn't save either text, and its usage events record only their sizes and the resulting level.
  • To check a PDF or Word resume, extract its text first with the file-to-text routes.