Skip to main content

Resumes

Upload a resume, parse it, and score it against a role.

Expert Hire developer workflow for Resumes
Build against the same evidence trail the product uses.

Three calls: ask for a destination, upload, register. Scoring is a fourth call because it costs an LLM round trip and a credit, and you usually want to pick the role first.

1. Ask for a destination

curl -X POST https://prep-api.experthire.cloud/v1/resumes/upload-url \
  -H "Authorization: Bearer $EH_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"candidate_id":"CANDIDATE_ID","filename":"ada-lovelace.pdf"}'

Still a placeholder: $EH_SECRET_KEY. Add it under Your values above.

{
  "object": "resume_upload",
  "upload_url": "https://storage.googleapis.com/...",
  "key": "resumes/CANDIDATE_ID/9f2a1c4b7e.pdf",
  "expires_in": 3600
}

Keep the key. You need it in step 3.

2. Upload the file

curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: application/pdf" \
  --data-binary @ada-lovelace.pdf

Still a placeholder: $UPLOAD_URL. Add it under Your values above.

The file goes to storage directly, never through the API, so large uploads do not hit request limits. PDF and DOCX are supported. The URL is valid for an hour.

3. Register it

Uploading a file does not create a resume. This is the call that parses it.

curl -X POST https://prep-api.experthire.cloud/v1/resumes \
  -H "Authorization: Bearer $EH_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"candidate_id":"CANDIDATE_ID","key":"KEY_FROM_STEP_1"}'

Still a placeholder: $EH_SECRET_KEY. Add it under Your values above.

The key is scoped to the candidate it was minted for; a key belonging to another candidate is rejected. A file we cannot read as a resume returns 422 unprocessable_resume.

4. Score it

curl -X POST https://prep-api.experthire.cloud/v1/resumes/RESUME_ID/score \
  -H "Authorization: Bearer $EH_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "role": "Senior Go Backend Engineer",
        "job_description": "Go, PostgreSQL, Kubernetes, distributed systems, payments domain. gRPC and Terraform a plus."
      }'

Still a placeholder: $EH_SECRET_KEY. Add it under Your values above.

{
  "object": "resume",
  "role": "Senior Go Backend Engineer",
  "overall_score": 85,
  "ats_readiness": 90,
  "domain_skill_fit": 93,
  "experience_relevance": 85,
  "missing_keywords": "payments domain knowledge",
  "strengths": "...",
  "weakness": "...",
  "improvements": "..."
}

Scoring takes 30 to 90 seconds. Set your client timeout accordingly.

role and job_description are optional, and they are what make the score role-weighted. The same resume against a Go backend brief scores 85 overall with a domain fit of 93; against a nursing brief it scores 17 with a domain fit of 5. Omit both and you get a standalone read of the resume with no role context.

Reading them back

curl -G https://prep-api.experthire.cloud/v1/resumes \
  -H "Authorization: Bearer $EH_SECRET_KEY" \
  --data-urlencode "candidate_id=CANDIDATE_ID"

Still a placeholder: $EH_SECRET_KEY. Add it under Your values above.

Newest first, same cursor rules as everywhere else. Drop candidate_id for the whole organisation. GET /v1/resumes/RESUME_ID returns one.

For the original file, ask for a link rather than the bytes:

curl https://prep-api.experthire.cloud/v1/resumes/RESUME_ID/file \
  -H "Authorization: Bearer $EH_SECRET_KEY"

Still a placeholder: $EH_SECRET_KEY. Add it under Your values above.

{ "object": "resume_file", "url": "https://storage.googleapis.com/...", "expires_in": 900 }

The link lasts 15 minutes. Mint a new one when it expires rather than storing it. file_name is only present for resumes uploaded through our own app; a file you registered over the API is stored under a generated key and has no original name.

DELETE /v1/resumes/RESUME_ID removes the record. The stored file stays: an interview created from the resume still references its analysis.

Re-scoring and cost

The role and job description are fingerprinted into a cache key, so scoring one resume against one brief charges one credit however many times you call it. Change the brief and it scores again, and charges again.

A call that does not run real scoring is not charged: an already-scored resume with no brief, or a cache hit, returns the existing analysis and refunds the reservation.

Getting the result

The response body above is the whole analysis, so polling is not required. If you would rather be told, register a webhook endpoint and listen for resume.scored. The payload is the identical shape, so one parser handles both.

Nothing is queued until an endpoint exists. See webhooks.