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.

Building with a coding agent?

Paste this whole reference with integration rules into Claude Code, Codex or Cursor, or add it to your agent as a skill.

Add skill copies this command. Run it in your project:

$ npx skills add dolevhayut/resurface

Works with Claude Code, Codex, Cursor and other agents. In the Claude apps, download the skill and upload it under Skills in settings. In Claude Code, the Resurface plugin adds the skill and MCP tools that score resumes directly.

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.

Retrying after a timeout? Send an Idempotency-Key header with a unique value per resume. Repeating the request with the same key within 24 hours returns the original result (with Idempotent-Replayed: true) instead of scoring and charging again.

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
import { readFile } from "node:fs/promises";

const form = new FormData();
form.append("job_title", "Senior Full Stack Engineer");
form.append("job_description", await readFile("job.txt", "utf8"));
// PDF, DOCX, HTML or TXT
form.append("resume", new Blob([await readFile("candidate.pdf")]), "candidate.pdf");

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 },
  "model": "jev-1.13.0",
  "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 stays unknown, not contradicted: it lowers this score and widens the range.
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 (line ids start at L000). Present for SUPPORTED and CONTRADICTED answers; null when the evidence is insufficient.
questions[].probabilities
object
Calibrated probability per verdict from the classifier.
questions[].computed
string
Years-of-experience questions only: how the years were counted (roles that count toward the requirement, overlaps merged).
must_have_gaps
string[]
Must-have questions without evidence.
must_have_contradicted
string[]
Must-have questions the resume rules out.
rubric
object
{ id, source, questions }. source is drafted:llm, rubric_cached, rubric or provided. Reuse rubric.id for the next resume.
resume
object
{ lines, language }: lines parsed from the file and the detected language.
model
string
The decision model that answered, for example jev-1.13.0, or clef-flash when the fallback answered.
billing
object
{ billable, price_usd, free_remaining, credits_remaining }. credits_remaining is your prepaid balance. Both balances are also in the X-Resurface-Free-Remaining and X-Resurface-Credits-Remaining headers.
latency_ms
number
Server time. A new job description adds a few seconds of drafting; a reused rubric takes about 0.25 s.

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, "credits_remaining": 500, "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
No free or prepaid scores left. Buy a score pack in the dashboard.
404
not_found
Unknown rubric_id.
413 / 415
invalid_request
File too large (over 5 MB), or an unsupported type such as legacy .doc.
429
rate_limited
More than 60 scores per minute.
409
idempotency_key_in_use
A request with the same Idempotency-Key is still running. Retry in a few seconds.
502
evaluator_unavailable
The classifier failed; the score is refunded automatically.
504
evaluator_timeout
Scoring took too long; the score is refunded automatically. Safe to retry.
{ "error": { "code": "free_tier_exhausted", "message": "…" } }

Claude & MCP

Score resumes from Claude Code or any MCP client. The MCP server calls the same API with the same key, so free scores, billing, limits and errors are the same as above.

Claude plugin

Resurface is in Claude's plugin directory: in Claude, open Customize, then Plugins, and search for Resurface. In Claude Code you can also install it with the commands below.

The plugin adds the MCP server and two skills: screen-candidates ranks a batch of resumes with quoted evidence, and resurface-api helps you build the API into your product. The first time Claude uses it, you sign in with your Resurface account (the same one as the dashboard); there is no key to copy.

in claude code
/plugin marketplace add dolevhayut/resurface
/plugin install resurface@resurface

Then ask, for example: “Rank the resumes in ./candidates against this job description.”

Any MCP client

Streamable HTTP at https://getresurface.dev/api/mcp. Clients that support MCP authorization sign in with your Resurface account (OAuth). Scripts, CI and other clients send your API key in the Authorization header instead.

claude code, without the plugin (sign in)
claude mcp add --transport http resurface https://getresurface.dev/api/mcp
with an API key instead
claude mcp add --transport http resurface https://getresurface.dev/api/mcp \
  --header "Authorization: Bearer rs_live_…"
cursor · ~/.cursor/mcp.json
{
  "mcpServers": {
    "resurface": {
      "url": "https://getresurface.dev/api/mcp"
    }
  }
}
score_resume
1 score
Scores one resume against job_description or rubric_id. Send resume_text, or resume_file_base64 with resume_filename. Returns the score, a verdict per question and the quoted evidence line.
draft_screening_questions
free
Turns a job description into 6–8 screening questions and returns a rubric id to reuse with score_resume. Cached per job text.
get_usage
free
Remaining free and prepaid scores, and scores used this month and in total.

Pricing & limits

50 free resume scores per developer, then prepaid score packs: 100 scores for $10, 500 for $40 or 2,500 for $150 (bought in the dashboard; prepaid scores are used after free ones). 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. Two-column PDFs are extracted in reading order.

EssentialAlways on

Sign-in cookies set by Clerk, this choice (stored in your browser), and the rs_promo cookie when you arrive through a launch offer. The site can't work without them.