Parse Resumes with Python
Extract structured resume data in Python with a single API call
To parse a resume in Python, send the PDF to POST https://api.resumeparser.cc/v1/parse with the requests library and read the JSON it returns: name, contact details, work experience, education and languages. It needs no SDK and no local NLP models, works on PDFs with a text layer up to 5 MB, and includes 50 free parses a month.
Parse PDF resumes with Python using the Resume Parser REST API. Setup with the requests library, rate-limit friendly batch processing, error handling, and structured JSON output.
Setup steps
- 1
Get your API key
Sign up at https://api.resumeparser.cc/signup with your email. You get a magic link, and you create an API key (rp_live_...) in the dashboard. Keep the key on your server and never expose it in client-side code.
- 2
Install the requests library
Run `pip install requests`. It handles the multipart/form-data encoding needed for file uploads.
- 3
Send a resume to the API
POST the PDF to https://api.resumeparser.cc/v1/parse as multipart field "file" with the header `Authorization: Bearer rp_live_...`. PDF only, up to 5 MB. Parsing is synchronous and can take up to 60 seconds, so set your HTTP client timeout to at least 60 seconds.
- 4
Use the JSON response
The response is `{ data, usage }`. data holds the parsed fields (firstName, lastName, emailAddress, phoneNumber, linkedInLink, workExperiences, educations, fluentLanguages, languages). usage holds billedAs, freeRemaining, and creditBalance.
Code examples
import os
import requests
API_URL = "https://api.resumeparser.cc/v1/parse"
API_KEY = os.environ["RESUME_PARSER_API_KEY"] # rp_live_...
MAX_BYTES = 5 * 1024 * 1024 # 5 MB
def parse_resume(file_path: str) -> dict:
if os.path.getsize(file_path) > MAX_BYTES:
raise ValueError("file exceeds 5 MB")
with open(file_path, "rb") as f:
response = requests.post(
API_URL,
headers={"Authorization": f"Bearer {API_KEY}"},
files={"file": (os.path.basename(file_path), f, "application/pdf")},
timeout=90, # parsing can take up to 60 seconds
)
# A proxy error can return HTML instead of JSON
if not response.headers.get("content-type", "").startswith("application/json"):
response.raise_for_status()
body = response.json()
if response.status_code == 422:
# No resume data found, usually a scanned PDF without a text layer. Not charged.
raise ValueError(f"could not parse {file_path}: {body['error']['message']}")
if not response.ok:
error = body["error"]
raise RuntimeError(f"{response.status_code} {error['code']}: {error['message']}")
return body
result = parse_resume("resume.pdf")
data = result["data"]
# Every field is optional: it is left out when the CV does not have it
print(data.get("firstName"), data.get("lastName"))
print(data.get("emailAddress"))
for experience in data.get("workExperiences", []):
print(experience["title"], "at", experience["organization"])
print(experience["startMonth"], experience["startYear"], "ongoing:", experience["isOngoing"])
for education in data.get("educations", []):
print(education["degree"], "at", education["school"])
print(result["usage"]) # billedAs, freeRemaining, creditBalanceimport os
import time
import json
from pathlib import Path
import requests
API_URL = "https://api.resumeparser.cc/v1/parse"
API_KEY = os.environ["RESUME_PARSER_API_KEY"]
# 30 requests per minute per key: one request every 2 seconds, one at a time
SECONDS_BETWEEN_REQUESTS = 2
output_dir = Path("parsed")
output_dir.mkdir(exist_ok=True)
for pdf_path in sorted(Path("resumes").glob("*.pdf")):
with open(pdf_path, "rb") as f:
response = requests.post(
API_URL,
headers={"Authorization": f"Bearer {API_KEY}"},
files={"file": (pdf_path.name, f, "application/pdf")},
timeout=60,
)
body = response.json()
if response.ok:
(output_dir / f"{pdf_path.stem}.json").write_text(json.dumps(body["data"], indent=2))
print(pdf_path.name, "ok, free remaining:", body["usage"]["freeRemaining"])
else:
print(pdf_path.name, "failed:", body["error"]["code"])
time.sleep(SECONDS_BETWEEN_REQUESTS)import os
import time
import requests
API_URL = "https://api.resumeparser.cc/v1/parse"
API_KEY = os.environ["RESUME_PARSER_API_KEY"]
# 429 rate_limited and 503 busy are worth retrying.
# 400 invalid_file, 401 invalid_api_key, 402 insufficient_credits and
# 413 file_too_large will fail again with the same request.
RETRYABLE_STATUSES = {429, 503}
def parse_resume_with_retry(file_path: str, max_attempts: int = 4) -> dict:
for attempt in range(1, max_attempts + 1):
with open(file_path, "rb") as f:
response = requests.post(
API_URL,
headers={"Authorization": f"Bearer {API_KEY}"},
files={"file": (os.path.basename(file_path), f, "application/pdf")},
timeout=60,
)
if response.ok:
return response.json()
error = response.json()["error"]
if response.status_code not in RETRYABLE_STATUSES or attempt == max_attempts:
raise RuntimeError(f"{response.status_code} {error['code']}: {error['message']}")
time.sleep(10 * attempt)
raise RuntimeError("unreachable")Open-source Python library or the API?
pyresparser is an open-source Python resume parser published on PyPI. It runs on your own machine with spaCy and NLTK rules; the API sends the PDF to our server and returns a fixed JSON schema. The library is free and keeps files local; the API costs per parse after the free 50 and needs no models or maintenance on your side.
| Option | Input | How it extracts | Fields | Licence and upkeep | Source |
|---|---|---|---|---|---|
| pyresparser | PDF and DOCX; DOC with textract installed | spaCy and NLTK, runs locally | Name, email, mobile number, skills, total experience, college, degree, designation, company names | GPL-3.0; last commit 13 September 2023 | GitHub |
| Resume Parser API | PDF with a text layer, up to 5 MB | Hosted model, one HTTPS request | Contact details, location, LinkedIn link, work experiences with month and year, educations, languages | 50 free parses a month, then EUR 0.15 to EUR 0.10 per parse | API docs |
Read on 2026-11-12 from each linked page. Vendors change formats and prices, so check the source before you decide.
Integration benefits
Only the requests library
No SDK to install. Any Python code that can send a multipart POST with a header can call the API.
One fixed JSON schema
Every response uses the same documented fields. Every field is optional, so check for missing values.
Works in batch jobs
Each request is independent. Stay within 30 requests per minute per key and retry 429 and 503 responses.
Failed parses are not charged
50 free parses per month, then prepaid credits. A request that ends in an error does not use a parse.
Frequently asked questions
Any Python that can make HTTPS requests. The examples use Python 3 with the requests library.
Send one request every 2 seconds per key, or use a small token bucket. If you get 429 rate_limited, wait for the Retry-After header (60 seconds) before retrying. If you get 503 busy, the API is at capacity; wait a few seconds and retry.
Yes. Use httpx or aiohttp with the same request, and keep an asyncio.Semaphore or a rate limiter so you stay within 30 requests per minute per key.
The API returns 400 invalid_file, or 422 parse_failed if parsing fails. Failed parses are not charged. Scanned image-only PDFs do not work because there is no OCR; the PDF needs a text layer.
Every parse response includes usage (billedAs, freeRemaining, creditBalance). You can also call GET https://api.resumeparser.cc/v1/usage with the same Authorization header.
More code examples
Parse Resumes with JavaScript and Node.js
Parse PDF resumes from Node.js using the Resume Parser REST API. Built-in fetch and FormData, TypeScript types, rate-limit friendly batch processing, and an Express upload route.
FeaturesSkill Extraction API
Get each role's description, with the skills used, tied to the job where they were used. PDF to structured JSON with one API call.
FeaturesPDF Resume Parser
Parse single-column and multi-column PDF resumes into structured JSON with one API call. PDF up to 5 MB. Parsing is synchronous and can take up to 60 seconds.
Start parsing resumes with Python
Sign up with your email, create an API key, and use the 50 free parses per month.