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`.
response.json
{
    "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.