← Blog

How to Build a Claude-Powered Resume Screener

2026-11-02 · 5 min read · SubToAPI Team

A Claude-powered resume screener takes unstructured resume text (or PDFs), sends it to Claude with a job description and scoring rubric, and gets back structured output — a fit score, a list of matched/missing skills, and a short rationale — that a recruiter can sort and review instead of reading every resume cold.

This article walks through the actual build: extracting text from resumes, designing a prompt that produces consistent structured output, running it as a batch job against a pile of candidates, and avoiding the two failure modes that kill most screening tools — inconsistent output format and silent bias. Everything here uses standard HTTP calls, so it works whether you're hitting Claude directly or through a wrapper API.

Why Claude instead of keyword matching

Traditional ATS keyword matching (counting occurrences of "Python" or "AWS") rewards resumes that are stuffed with buzzwords and misses candidates who describe the same experience differently. Claude reads the resume the way a hiring manager would: it can tell the difference between "led a team of 5 engineers" and "was on a team," infer seniority from context, and compare actual responsibilities against the job description rather than just vocabulary overlap.

The tradeoff is that you need to control the output shape carefully, because an LLM left to write freeform will give you inconsistent prose that's hard to rank or filter.

Architecture

The pipeline has four stages:

  1. Ingest — pull resume text out of PDFs/DOCX (use a library like pdf-parse or mammoth, not Claude, for this — it's cheaper and more reliable)
  2. Score — send resume text + job description to Claude, get back a structured JSON verdict
  3. Store — save the score, rationale, and matched skills in your database
  4. Rank — sort candidates by score for human review

Claude only does step 2. Don't ask it to extract text from a raw PDF buffer — convert to plain text first.

Designing the prompt

The key to reliable output is giving Claude a strict schema and asking it to return only JSON. Here's a prompt template that works well in practice:

You are screening a resume against a job description. Score objectively based only on relevant experience, skills, and qualifications stated in the resume. Do not infer or penalize based on name, age, gender, location, school, or photo.

Job Description:
{{job_description}}

Resume:
{{resume_text}}

Return ONLY valid JSON in this exact shape:
{
  "fit_score": <integer 0-100>,
  "matched_skills": [<string>],
  "missing_skills": [<string>],
  "years_relevant_experience": <number>,
  "rationale": "<2-3 sentence summary>",
  "flags": [<string>]
}

The explicit instruction to ignore demographic signals matters — it won't eliminate bias entirely, but it reduces the model's tendency to let proxies like school name or location quietly influence the score. Put this instruction in every prompt, not just once in a system message you might forget to reuse.

The flags field is useful for catching things like employment gaps, certifications nearing expiry, or visa-sponsorship requirements mentioned in the resume — things you want surfaced but not baked into the numeric score.

Calling the API

If you're using SubToAPI to turn your existing Claude access into a REST endpoint, the call looks like this:

curl https://api.subtoapi.app/v1/messages \
  -H "Authorization: Bearer $SUBTOAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-sonnet-4-5",
    "max_tokens": 500,
    "messages": [
      {
        "role": "user",
        "content": "You are screening a resume... [full prompt with job_description and resume_text filled in]"
      }
    ]
  }'

For higher reliability, define the output schema as a tool and force Claude to call it — this guarantees valid JSON instead of relying on instructions alone. See /docs/tools for the tool-use format; the short version is you pass a tools array with an input_schema matching the fields above, and Claude returns a tool_use block instead of free text.

const response = await fetch('https://api.subtoapi.app/v1/messages', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.SUBTOAPI_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    model: 'claude-sonnet-4-5',
    max_tokens: 500,
    tools: [{
      name: 'score_resume',
      input_schema: {
        type: 'object',
        properties: {
          fit_score: { type: 'integer' },
          matched_skills: { type: 'array', items: { type: 'string' } },
          missing_skills: { type: 'array', items: { type: 'string' } },
          years_relevant_experience: { type: 'number' },
          rationale: { type: 'string' },
          flags: { type: 'array', items: { type: 'string' } }
        },
        required: ['fit_score', 'matched_skills', 'rationale']
      }
    }],
    tool_choice: { type: 'tool', name: 'score_resume' },
    messages: [
      { role: 'user', content: `Job Description:\n${jobDescription}\n\nResume:\n${resumeText}` }
    ]
  })
});

const data = await response.json();
const result = data.content.find(b => b.type === 'tool_use').input;

Forcing tool_choice to the scoring tool removes the "please return JSON" guesswork entirely. Check /docs/messages for the general request/response format and /docs/quickstart if you're setting up API access for the first time.

Batch processing a pipeline of candidates

For a real hiring pipeline you're scoring dozens or hundreds of resumes against the same job description. Run them concurrently but rate-limit your batch (10–20 in flight at once is usually safe), store raw Claude output alongside the parsed score for auditing, and re-run the same resume through the same prompt occasionally to spot-check consistency — scores should stay within a few points run to run.

Cache the job description scoring prompt separately from the resume text so you can swap in a new rubric without re-touching your ingestion code.

Keeping it fair and auditable

A few practices that matter more than most teams expect:

A screener like this costs very little to run per resume. If you're scoring in bulk, a Solo plan (€9) covers light usage; teams running continuous pipelines usually land on Team (€19/seat) for shared dashboards and usage visibility — see /pricing for details.

questions

Does Claude read PDF resumes directly? Not reliably as raw files through the API — extract text first with a PDF/DOCX parsing library, then send the plain text to Claude. This also keeps costs down since you're not paying for image-token processing.

How do I stop the screener from being biased? Explicitly instruct the model to ignore name, age, gender, and location in the prompt, strip photos and names during ingestion when possible, and always keep a human reviewing the top-ranked candidates rather than auto-rejecting based on score alone.

Can I use this for high-volume recruiting pipelines? Yes — batch the requests with a concurrency limit, force structured output via tool use (see /docs/tools) for consistent parsing, and store raw rationale text for auditing alongside the numeric score.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →