API reference

Base URL https://getresurface.dev/api/v1. Requests and responses are JSON, except that /score also accepts multipart/form-data for file uploads.

Authentication

Sign up and create a key in the dashboard (50 free scores). Send it on every request:

Authorization: Bearer rs_live_…

POST /v1/score

Scores one resume against a job. Describe the job in one of three ways. With a job_description, questions are drafted once and cached as a rubric; the response returns its rubric.id so you can reuse it.

resume
file
PDF, DOCX, HTML or TXT, up to 5 MB (multipart). Or send resume_text.
resume_text
string
Plain-text resume, instead of a file.
job_description
string
Full job description (≥ 40 chars). Questions are drafted and cached.
job_title
string
Optional; improves question drafting.
rubric_id
string
Reuse questions from POST /v1/rubrics or a previous score.
questions
array
Bring your own: ["Has hands-on React experience?", …] or objects { question, requirement: "must"|"nice", weight, min_years }. Max 12.
curl
curl https://getresurface.dev/api/v1/score \
  -H "Authorization: Bearer $RESURFACE_API_KEY" \
  -F job_title="Senior Full Stack Engineer" \
  -F job_description=@job.txt \
  -F resume=@candidate.pdf
JavaScript
const form = new FormData();
form.append("job_title", "Senior Full Stack Engineer");
form.append("job_description", jobDescription);
form.append("resume", resumeFile); // PDF, DOCX, HTML or TXT

const res = await fetch("https://getresurface.dev/api/v1/score", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.RESURFACE_API_KEY}` },
  body: form,
});
const { score, match, questions } = await res.json();
Python
import os, requests

r = requests.post(
    "https://getresurface.dev/api/v1/score",
    headers={"Authorization": f"Bearer {os.environ['RESURFACE_API_KEY']}"},
    data={"job_title": "Senior Full Stack Engineer", "job_description": open("job.txt").read()},
    files={"resume": open("candidate.pdf", "rb")},
)
print(r.json()["score"], r.json()["match"])
200 response
{
  "object": "score",
  "id": "scr_Qm3xk2Lp9aRt",
  "score": 92,
  "match": "strong_evidence",
  "coverage": 0.917,
  "range": { "lower": 92, "upper": 100 },
  "must_have_gaps": [],
  "questions": [
    {
      "id": "react_in_production",
      "question": "Has the candidate built production front-end applications with React?",
      "requirement": "must",
      "weight": 3,
      "verdict": "SUPPORTED",
      "status": "supported",
      "evidence": { "line": "L006", "text": "- Built dashboards in React and TypeScript for 40k users" },
      "probabilities": { "SUPPORTED": 0.97, "INSUFFICIENT_EVIDENCE": 0.03, "CONTRADICTED": 0 },
      "confidence": 0.96
    }
  ],
  "rubric": { "id": "rub_8fKq2", "source": "rubric_cached", "questions": 6 },
  "billing": { "billable": false, "price_usd": 0, "free_remaining": 49 },
  "latency_ms": 640
}
score
0–100
Share of question weight backed by a quoted resume line. Missing information never counts as a failure.
match
enum
strong_evidence · needs_verification · lower_evidence
coverage
0–1
Share of question weight with a definite answer (supported or contradicted).
range
object
Lower bound = score; upper bound if every not-established question turned out true.
questions[].verdict
enum
SUPPORTED · CONTRADICTED · INSUFFICIENT_EVIDENCE
questions[].evidence
object|null
{ line, text }: a verbatim line from the resume.
questions[].probabilities
object
Calibrated probability per verdict from the classifier.

POST /v1/rubrics

Drafts 6–8 atomic screening questions from a job description, each tied to a verbatim span of the posting. Free, and cached per job text. Pass the returned id as rubric_id.

curl
curl https://getresurface.dev/api/v1/rubrics \
  -H "Authorization: Bearer $RESURFACE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"job_title": "Data Engineer", "job_description": "Requirements:\n- 3+ years in data engineering\n- Advanced SQL\n- Spark or dbt pipelines"}'

GET /v1/rubrics/:id returns a saved rubric.

GET /v1/usage

Free scores remaining, scores this month, and the amount due.

{ "free_remaining": 37, "scores": { "total": 13, "this_month": 13, "billable": 0 }, "price_per_score_usd": 0.1, "amount_due_usd": 0 }

Errors

400
invalid_request
Missing job or resume, or an unreadable file.
401
invalid_api_key
Missing, invalid or revoked key.
402
free_tier_exhausted
The 50 free scores are used up and billing isn't enabled yet.
404
not_found
Unknown rubric_id.
413 / 415
file
File too large, or an unsupported type.
429
rate_limited
More than 60 scores per minute.
502
evaluator_unavailable
The classifier failed; free credits are refunded automatically.
{ "error": { "code": "free_tier_exhausted", "message": "…" } }

Pricing & limits

50 free resume scores per developer, then $0.10 per score. Rubric drafting is free. Limits: 60 scores per minute, files up to 5 MB, up to 12 custom questions. Scanned (image-only) PDFs are not supported yet. Hebrew and other right-to-left resumes are extracted in correct reading order, including PDFs that mix Hebrew and English.