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

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.