Documentation
API documentation
Use a `cv:parse` token to send PDF, DOC, DOCX, JPG, or PNG CVs. The default flow is asynchronous with polling, and the same endpoint also supports a synchronous base-parse mode for clients that need an immediate result. Optional analysis is available in async mode, including the five analysis credits in the Free Trial.
Request contract
- Method
- POST
- URL
- /api/cv-parser/parses
- Polling
- GET /api/cv-parser/parses/{cvParse}
- Default mode
- `async` returns `202 Accepted` for new work and `200 OK` for cache hits.
- Optional sync mode
- `mode=sync` runs a base parse inline and returns `200 OK` on success or `504` if parsing exceeds the 8 second sync timeout.
- Auth
- Authorization: Bearer sk_cv_...
- File support
- PDF, DOC, DOCX, JPG, and PNG CVs
- Retention
- Encrypted parser artifacts expire after 7 days
- Optional features
- Workspaces can send `features[]=analysis` in async mode when they want seniority and consistency checks for that CV.
Request samples
Examples of the async upload plus polling flow.
Bash request.sh
api_base_url="https://www.cvlens.ai"
response=$(curl -sS -X POST "$api_base_url/api/cv-parser/parses" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "cv=@resume.docx" \
-F "features[]=analysis")
poll_url=$(printf "%s" "$response" | jq -r '.poll_url')
case "$poll_url" in
http*) ;;
*) poll_url="$api_base_url$poll_url" ;;
esac
status=$(printf "%s" "$response" | jq -r '.status')
while [ "$status" = "queued" ] || [ "$status" = "processing" ]; do
sleep 2
response=$(curl -sS -X GET "$poll_url" \
-H "Authorization: Bearer YOUR_API_KEY")
status=$(printf "%s" "$response" | jq -r '.status')
done
printf "%s\n" "$response"
PHP request.php
<?php
$apiBaseUrl = 'https://www.cvlens.ai';
$apiKey = 'YOUR_API_KEY';
function sendRequest(string $url, string $apiKey, ?string $filePath = null): array
{
$curl = curl_init($url);
$headers = [
'Authorization: Bearer '.$apiKey,
'Accept: application/json',
];
$options = [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_TIMEOUT => 60,
];
if ($filePath !== null) {
$options[CURLOPT_POST] = true;
$options[CURLOPT_POSTFIELDS] = [
'cv' => new CURLFile($filePath, 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', basename($filePath)),
'features[]' => 'analysis',
];
}
curl_setopt_array($curl, $options);
$rawResponse = curl_exec($curl);
if ($rawResponse === false) {
throw new RuntimeException(curl_error($curl));
}
$statusCode = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
if ($statusCode >= 400) {
throw new RuntimeException($rawResponse);
}
return json_decode($rawResponse, true, 512, JSON_THROW_ON_ERROR);
}
$response = sendRequest("{$apiBaseUrl}/api/cv-parser/parses", $apiKey, 'resume.docx');
$pollUrl = str_starts_with($response['poll_url'], 'http') ? $response['poll_url'] : $apiBaseUrl.$response['poll_url'];
while (in_array($response['status'], ['queued', 'processing'], true)) {
sleep(2);
$response = sendRequest($pollUrl, $apiKey);
}
echo json_encode($response, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR).PHP_EOL;
Node request.mjs
import { openAsBlob } from 'node:fs';
const apiBaseUrl = 'https://www.cvlens.ai';
const apiKey = 'YOUR_API_KEY';
const form = new FormData();
form.set('cv', await openAsBlob('resume.docx'), 'resume.docx');
form.append('features[]', 'analysis');
let response = await fetch(`${apiBaseUrl}/api/cv-parser/parses`, {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
},
body: form,
}).then(async (result) => {
if (!result.ok) {
throw new Error(await result.text());
}
return result.json();
});
let pollUrl = response.poll_url.startsWith('http')
? response.poll_url
: `${apiBaseUrl}${response.poll_url}`;
while (response.status === 'queued' || response.status === 'processing') {
await new Promise((resolve) => setTimeout(resolve, 2_000));
response = await fetch(pollUrl, {
headers: {
Authorization: `Bearer ${apiKey}`,
},
}).then(async (result) => {
if (!result.ok) {
throw new Error(await result.text());
}
return result.json();
});
}
console.log(JSON.stringify(response, null, 2));
Python request.py
import json
import time
import requests
api_base_url = "https://www.cvlens.ai"
api_key = "YOUR_API_KEY"
with open("resume.docx", "rb") as cv_file:
response = requests.post(
f"{api_base_url}/api/cv-parser/parses",
headers={"Authorization": f"Bearer {api_key}"},
files={"cv": ("resume.docx", cv_file)},
data={"features[]": "analysis"},
timeout=60,
)
response.raise_for_status()
payload = response.json()
poll_url = payload["poll_url"]
if not poll_url.startswith("http"):
poll_url = f"{api_base_url}{poll_url}"
while payload["status"] in {"queued", "processing"}:
time.sleep(2)
response = requests.get(
poll_url,
headers={"Authorization": f"Bearer {api_key}"},
timeout=60,
)
response.raise_for_status()
payload = response.json()
print(json.dumps(payload, indent=2))
Java Request.java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
HttpClient client = HttpClient.newHttpClient();
String apiBaseUrl = "https://www.cvlens.ai";
String apiKey = "YOUR_API_KEY";
String boundary = "----CVLensBoundary";
byte[] fileBytes = Files.readAllBytes(Path.of("resume.docx"));
String multipartBody =
"--" + boundary + "\r\n"
+ "Content-Disposition: form-data; name=\"cv\"; filename=\"resume.docx\"\r\n"
+ "Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document\r\n\r\n"
+ new String(fileBytes, StandardCharsets.ISO_8859_1) + "\r\n"
+ "--" + boundary + "\r\n"
+ "Content-Disposition: form-data; name=\"features[]\"\r\n\r\n"
+ "analysis\r\n"
+ "--" + boundary + "--\r\n";
HttpRequest createRequest = HttpRequest.newBuilder()
.uri(URI.create(apiBaseUrl + "/api/cv-parser/parses"))
.timeout(Duration.ofSeconds(60))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
.POST(HttpRequest.BodyPublishers.ofString(multipartBody, StandardCharsets.ISO_8859_1))
.build();
HttpResponse<String> response = client.send(createRequest, HttpResponse.BodyHandlers.ofString());
String payload = response.body();
Pattern pollUrlPattern = Pattern.compile("\"poll_url\"\\s*:\\s*\"([^\"]+)\"");
Pattern statusPattern = Pattern.compile("\"status\"\\s*:\\s*\"([^\"]+)\"");
Matcher pollUrlMatcher = pollUrlPattern.matcher(payload);
if (!pollUrlMatcher.find()) {
throw new IllegalStateException("poll_url missing from response");
}
String pollUrl = pollUrlMatcher.group(1);
if (!pollUrl.startsWith("http")) {
pollUrl = apiBaseUrl + pollUrl;
}
String status = extractStatus(statusPattern, payload);
while (status.equals("queued") || status.equals("processing")) {
Thread.sleep(2000);
HttpRequest pollRequest = HttpRequest.newBuilder()
.uri(URI.create(pollUrl))
.timeout(Duration.ofSeconds(60))
.header("Authorization", "Bearer " + apiKey)
.GET()
.build();
response = client.send(pollRequest, HttpResponse.BodyHandlers.ofString());
payload = response.body();
status = extractStatus(statusPattern, payload);
}
System.out.println(payload);
static String extractStatus(Pattern statusPattern, String payload) {
Matcher statusMatcher = statusPattern.matcher(payload);
if (!statusMatcher.find()) {
throw new IllegalStateException("status missing from response");
}
return statusMatcher.group(1);
}
Multipart fields
- `cv`
- Required uploaded file. Supports PDF, DOC, DOCX, JPG, and PNG.
- `force_reparse`
- Optional boolean. Skips cache reuse and forces a fresh base parse.
- `mode`
- Optional string. Use `async` for the default queue-plus-polling flow or `sync` for an inline base parse.
- `features[]`
- Optional array for async requests. Currently supports only `analysis`. Sync requests must omit `features[]`.
Billing behavior
25 one-time parses. Analysis included on 5 of them. Default has a €5 monthly minimum spend, fully credited toward metered usage.
Default
€5 monthly minimum spend
Fully credited toward metered usage
€0.05 async parse
€0.06 sync parse
+ €0.10 analysis
Business
€15 monthly base
€0.02 async parse
€0.03 sync parse
+ €0.04 analysis
Business 10k+
€115 monthly minimum
€15 platform + €100 parse commitment
€0.01 async parse
€0.02 sync parse
+ €0.04 analysis
Failed parses and full cache hits are free. Analysis is available in async mode only. A new analysis on a cached base parse consumes only analysis usage. Free Trial responses use `period=lifetime` and `scope=lifetime`; paid plans use `period=YYYY-MM` and `scope=billing_month`. The nested `usage.analysis` object exposes the same `used`, `reserved`, `limit`, `remaining`, `period`, and `scope` fields as parse usage.
Business 10k+ formula
€15 platform + max(€100 parse commitment, metered parse usage) + metered analysis
No automatic switch occurs at 10,000 parses. The commitment tier is assigned by contract, and unused commitment expires each billing month.
Errors and limits
- 401
- Missing or invalid Bearer token.
- 403
- The workspace has reached its 25 one-time Free Trial parse credits or 5 one-time analysis credits.
- 422
- Upload validation failed, for example unsupported file type, unknown feature, or `mode=sync` combined with `features[]=analysis`.
- 429
- The API key or source IP exceeded the parser rate limit.
- 504
- A sync request exceeded the configured timeout. The parse record is persisted as failed with `code=sync_timeout`.
{
"id": "01HV7N8F6K9P2M3Q4R5S6T7U8V",
"status": "completed",
"cache_hit": false,
"poll_url": "/api/cv-parser/parses/01HV7N8F6K9P2M3Q4R5S6T7U8V",
"warnings": [
"text_truncated_for_prompt_budget"
],
"error": null,
"data": {
"person": {
"full_name": "Jane Miller",
"first_name": "Jane",
"last_name": "Miller",
"email": "jane.miller@example.test",
"phone": "+49 30 1234567",
"birth_date": "1992-08-12",
"driver_license": {
"has_license": true,
"categories": [
"B"
]
},
"social_links": [
{
"type": "linkedin",
"label": "LinkedIn",
"url": "https://linkedin.com/in/jane-miller"
},
{
"type": "github",
"label": "GitHub",
"url": "https://github.com/jane-miller"
}
],
"location": {
"raw": "Berlin, Germany",
"city": "Berlin",
"region": "Berlin",
"postal_code": "10115",
"country": "Germany",
"country_code": "DE",
"remote_preference": "hybrid"
}
},
"summary": "Senior product engineer with experience building API-first SaaS products.",
"skills": [
"PHP",
"Laravel",
"REST APIs",
"PostgreSQL",
"Team leadership"
],
"experience": [
{
"company": "Example SaaS GmbH",
"title": "Senior Product Engineer",
"start_date": "2021-04",
"end_date": null,
"location": "Berlin, Germany",
"highlights": [
"Led the migration of a parser API to an event-driven architecture.",
"Improved document processing throughput by 35 percent."
]
}
],
"education": [
{
"institution": "Technical University of Example",
"degree": "M.Sc.",
"field_of_study": "Computer Science",
"start_date": "2014-10",
"end_date": "2016-09"
}
],
"languages": [
{
"name": "German",
"proficiency": "native"
},
{
"name": "English",
"proficiency": "professional"
}
],
"certifications": [
{
"name": "AWS Certified Developer - Associate",
"issuer": "Amazon Web Services",
"issued_at": "2023-05"
}
],
"document": {
"document_language": "en",
"document_type": "cv"
}
},
"analysis": {
"seniority_assessment": {
"level": "senior",
"confidence": "high",
"summary": "The CV shows sustained ownership of complex backend systems and mentoring responsibilities."
},
"consistency_checks": {
"timeline_gaps": [
{
"severity": "low",
"message": "No material employment gaps detected.",
"evidence": [
"Experience entries cover 2016-10 through present."
],
"confidence": "medium"
}
],
"date_conflicts": [
{
"severity": "low",
"message": "Education and first full-time role overlap by one month.",
"evidence": [
"Education ends 2016-09.",
"First role starts 2016-09."
],
"confidence": "medium"
}
],
"overlapping_roles": [
{
"severity": "info",
"message": "No conflicting overlapping full-time roles found.",
"evidence": [],
"confidence": "high"
}
],
"missing_information": [
{
"severity": "medium",
"message": "Certification credential ID is not listed.",
"evidence": [
"Certification includes issuer and issue date only."
],
"confidence": "high"
}
],
"questionable_claims": [
{
"severity": "low",
"message": "Throughput improvement claim should be verified during screening.",
"evidence": [
"Improved document processing throughput by 35 percent."
],
"confidence": "medium"
}
]
}
},
"usage": {
"used": 42,
"reserved": 1,
"limit": null,
"remaining": null,
"period": "2026-04",
"scope": "billing_month",
"analysis": {
"used": 8,
"reserved": 0,
"limit": null,
"remaining": null,
"period": "2026-04",
"scope": "billing_month"
}
}
}
Structured output notes
- `data` contains extracted CV facts only, whether the parse completed via async polling or `mode=sync`.
- `analysis` is always part of the response contract, but stays `null` unless you requested `features[]=analysis`.
- `code` is present on failed parses and exposes the failure code such as `sync_timeout`.
- `data.person.location` is now structured instead of a single flat string.
- `data.person` can include birth date, driver license, and structured social links when the document contains them.
- `data.document.document_language` gives you a normalized language hint for the uploaded CV.