On this page
Jump directly to the API documentation section you need.
The CVME Format DOCX API allows an authorised company integration to submit a CV file, process it through the CVME formatting pipeline, and retrieve a formatted DOCX document asynchronously.
Jump directly to the API documentation section you need.
All API requests must include a company API key.
Authorization: Api-Key YOUR_API_KEY
X-CVME-API-Key: YOUR_API_KEY
export CVME_API_KEY="cvme_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{
"success": true,
"job_id": "a2bbc740-758c-450e-b2ec-1f49ec3d9d06",
"status": "queued"
}
{
"success": false,
"error": {
"code": "missing_file",
"message": "file is required."
}
}
Download endpoints return a binary DOCX file on success. On failure, they return the standard JSON error format.
Submit a CV file using multipart/form-data. The response returns a queued job with status and download URLs.
| Field | Type | Description |
|---|---|---|
template_id |
integer | The CVME template ID to use. |
file |
file | The CV file to format. Supported input depends on your converter, for example PDF, DOCX, DOC, or TXT. |
| Field | Type | Default | Description |
|---|---|---|---|
target_language |
string | en-GB |
Target output language the document will be converted to. See below for full list of supported languages |
perspective_control |
string | None |
Optional perspective control, for example First which will output the document as written in the first person or Third which will output the document as written in the third person. The standard behaviour will be to leave the document as written originally. |
anonymise |
boolean | false |
Whether to anonymise the CV. |
anonymise_scope |
string | all |
Which roles to anonymise. Supported values: 1, 3, all. This allows you to control whether you would like to anonymise just the first role, the first three roles or all roles. |
note_merge_text |
string | empty | Notes or instructions to merge into the CV. |
note_merge_file |
file | none | File containing notes or instructions to merge such as a call transcript. |
optimise_text |
string | empty | Job description or target brief for optimisation. |
optimise_file |
file | none | File containing optimisation or job-description text. |
notes_enabled |
boolean | false |
If true, optimisation summary can be placed into consultant notes. |
candidate_mode |
string | candidate |
Mode passed into the merge pipeline. |
abridge_cv |
boolean | false |
Whether optimisation should abridge the CV. |
merge_options |
JSON string | {} |
Controls note-merge behaviour. Must be valid JSON. |
The target_language field accepts the following values. The default is English (British).
AmharicArabicBengaliBosnianBulgarianChineseChinese (Simplified)Chinese (Traditional)CroatianCzechDanishDutchEnglish (American)English (British)EstonianFinnishFrenchGermanGreekHausaHebrewHindiHungarianIcelandicIgboIndonesianItalianJapaneseKoreanLatvianLithuanianMalayMarathiNorwegianPersian (Farsi)PolishPortugueseRomanianRussianSerbianSlovakSlovenianSpanish (Spain)Spanish (Mexico)Spanish (Argentina)Spanish (United States)SwahiliSwedishTamilTeluguThaiTurkishUkrainianUrduVietnameseXhosaYorubaZulu
The merge_options field is an optional JSON string used with note_merge_text or note_merge_file. It controls which parts of the existing CV can be updated by the note merge stage.
| Option | Type | Default | Description |
|---|---|---|---|
merge_summary |
boolean | true |
Allows the note merge stage to update the summary or consultant notes. |
merge_experience |
boolean | true |
Allows the note merge stage to update existing work experience and add new work experience where appropriate. |
merge_skills_qualifications |
boolean | true |
Allows the note merge stage to update skills and qualifications. |
merge_cover_sheet |
boolean | true |
Allows the note merge stage to update matching cover sheet fields. |
merge_options must be sent as a valid JSON string inside the multipart form request. Invalid JSON returns invalid_merge_options.
{
"merge_summary": true,
"merge_experience": true,
"merge_skills_qualifications": true,
"merge_cover_sheet": true
}
{
"merge_summary": true,
"merge_experience": false,
"merge_skills_qualifications": false,
"merge_cover_sheet": false
}
{
"merge_summary": true,
"merge_experience": true,
"merge_skills_qualifications": true,
"merge_cover_sheet": false
}
curl -i -X POST "https://cvme.ai/app/format_docx_api/" \
-H "Authorization: Api-Key $CVME_API_KEY" \
-F "template_id=189" \
-F "target_language=English (British)" \
-F "note_merge_text=The candidate is immediately available and has recently completed additional cloud-focused project work. Add this where appropriate without duplicating existing content." \
-F 'merge_options={"merge_summary":true,"merge_experience":true,"merge_skills_qualifications":true,"merge_cover_sheet":false}' \
-F "file=@CVME_Test.pdf"
The job is asynchronous. A 202 Accepted response means the job has been queued, not completed.
{
"success": true,
"job_id": "a2bbc740-758c-450e-b2ec-1f49ec3d9d06",
"status": "queued",
"status_url": "https://cvme.ai/app/format_docx_api/jobs/a2bbc740-758c-450e-b2ec-1f49ec3d9d06/",
"download_url": "https://cvme.ai/app/format_docx_api/jobs/a2bbc740-758c-450e-b2ec-1f49ec3d9d06/download/",
"webhook_enabled": true,
"expires_at": "2026-09-04T13:42:10.123456+01:00",
"steps": {
"format": true,
"anonymise": true,
"anonymise_scope": "all",
"note_merge": true,
"optimise": true
}
}
curl -i "https://cvme.ai/app/format_docx_api/jobs/a2bbc740-758c-450e-b2ec-1f49ec3d9d06/" \
-H "Authorization: Api-Key $CVME_API_KEY"
{
"success": true,
"job_id": "a2bbc740-758c-450e-b2ec-1f49ec3d9d06",
"status": "queued",
"input_filename": "CVME_Test.pdf",
"filename": "",
"created_at": "2026-09-03T13:42:10.123456+01:00",
"updated_at": "2026-09-03T13:42:10.123456+01:00",
"completed_at": null,
"expires_at": "2026-09-04T13:42:10.123456+01:00",
"expired": false,
"webhook_enabled": true,
"webhook_delivered": false,
"webhook_attempts": 0,
"webhook_last_error": ""
}
{
"success": true,
"job_id": "a2bbc740-758c-450e-b2ec-1f49ec3d9d06",
"status": "complete",
"input_filename": "CVME_Test.pdf",
"filename": "cvme-test-anonymised.docx",
"created_at": "2026-09-03T13:42:10.123456+01:00",
"updated_at": "2026-09-03T13:46:20.123456+01:00",
"completed_at": "2026-09-03T13:46:20.123456+01:00",
"expires_at": "2026-09-04T13:46:20.123456+01:00",
"expired": false,
"webhook_enabled": true,
"webhook_delivered": true,
"webhook_attempts": 1,
"webhook_last_error": "",
"download_url": "https://cvme.ai/app/format_docx_api/jobs/a2bbc740-758c-450e-b2ec-1f49ec3d9d06/download/"
}
{
"success": true,
"job_id": "a2bbc740-758c-450e-b2ec-1f49ec3d9d06",
"status": "failed",
"input_filename": "CVME_Test.pdf",
"filename": "",
"created_at": "2026-09-03T13:42:10.123456+01:00",
"updated_at": "2026-09-03T13:46:20.123456+01:00",
"completed_at": "2026-09-03T13:46:20.123456+01:00",
"expires_at": "2026-09-04T13:42:10.123456+01:00",
"expired": false,
"webhook_enabled": true,
"webhook_delivered": true,
"webhook_attempts": 1,
"webhook_last_error": "",
"error": {
"code": "processing_failed",
"message": "Formatting failed."
}
}
curl -L "https://cvme.ai/app/format_docx_api/jobs/a2bbc740-758c-450e-b2ec-1f49ec3d9d06/download/" \
-H "Authorization: Api-Key $CVME_API_KEY" \
--output formatted-cv.docx
Returns a binary DOCX file with headers similar to:
Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document
Content-Disposition: attachment; filename="cvme-test-anonymised.docx"
Cache-Control: private, no-store
Pragma: no-cache
X-Content-Type-Options: nosniff
HTTP 409 Conflict
{
"success": false,
"error": {
"code": "document_not_ready",
"message": "Document is not ready yet."
},
"job_id": "a2bbc740-758c-450e-b2ec-1f49ec3d9d06",
"job_status": "processing"
}
HTTP 410 Gone
{
"success": false,
"error": {
"code": "download_expired",
"message": "This download has expired."
},
"job_id": "a2bbc740-758c-450e-b2ec-1f49ec3d9d06",
"expires_at": "2026-09-04T13:46:20.123456+01:00"
}
HTTP 404 Not Found
{
"success": false,
"error": {
"code": "file_not_found",
"message": "Generated file not found."
},
"job_id": "a2bbc740-758c-450e-b2ec-1f49ec3d9d06"
}
curl -i "https://cvme.ai/app/format_docx_api/templates/" \
-H "Authorization: Api-Key $CVME_API_KEY"
{
"success": true,
"count": 2,
"templates": [
{
"id": 189,
"name": "Standard CV Template"
},
{
"id": 201,
"name": "Public Template"
}
]
}
If the company integration has a default callback URL and webhook secret, CVME sends a webhook when the job completes or fails. The webhook does not attach the DOCX file. It provides a download URL, which must be called with the API key.
format_docx.complete
format_docx.failed
Content-Type: application/json
X-CVME-Event: format_docx.complete
X-CVME-Timestamp: 1788439580
X-CVME-Signature: sha256=...
CVME signs webhook requests so your application can verify that the webhook was sent by CVME and that the request body has not been changed in transit.
The signing method is HMAC SHA-256. Your webhook secret is kept private and is used to verify the signature.
message = "{timestamp}.{raw_body}"
signature = HMAC_SHA256(webhook_secret, message)
Compare the generated signature with the value supplied in X-CVME-Signature. Reject the webhook if the signature does not match or if the timestamp is outside your allowed tolerance window.
{
"event": "format_docx.complete",
"job_id": "a2bbc740-758c-450e-b2ec-1f49ec3d9d06",
"status": "complete",
"input_filename": "CVME_Test.pdf",
"filename": "cvme-test-anonymised.docx",
"created_at": "2026-09-03T13:42:10.123456+01:00",
"updated_at": "2026-09-03T13:46:20.123456+01:00",
"completed_at": "2026-09-03T13:46:20.123456+01:00",
"expires_at": "2026-09-04T13:46:20.123456+01:00",
"status_url": "https://cvme.ai/app/format_docx_api/jobs/a2bbc740-758c-450e-b2ec-1f49ec3d9d06/",
"download_url": "https://cvme.ai/app/format_docx_api/jobs/a2bbc740-758c-450e-b2ec-1f49ec3d9d06/download/",
"error": ""
}
{
"event": "format_docx.failed",
"job_id": "a2bbc740-758c-450e-b2ec-1f49ec3d9d06",
"status": "failed",
"input_filename": "CVME_Test.pdf",
"filename": "CVME_Test.pdf",
"created_at": "2026-09-03T13:42:10.123456+01:00",
"updated_at": "2026-09-03T13:46:20.123456+01:00",
"completed_at": "2026-09-03T13:46:20.123456+01:00",
"expires_at": "2026-09-04T13:42:10.123456+01:00",
"status_url": "https://cvme.ai/app/format_docx_api/jobs/a2bbc740-758c-450e-b2ec-1f49ec3d9d06/",
"download_url": "",
"error": "Formatting failed."
}
import hashlib
import hmac
import time
def verify_cvme_webhook(raw_body, headers, webhook_secret):
signature_header = headers.get("X-CVME-Signature", "")
timestamp = headers.get("X-CVME-Timestamp", "")
if not signature_header.startswith("sha256="):
return False
try:
timestamp_int = int(timestamp)
except Exception:
return False
if abs(time.time() - timestamp_int) > 300:
return False
expected = hmac.new(
webhook_secret.encode("utf-8"),
f"{timestamp}.{raw_body}".encode("utf-8"),
hashlib.sha256,
).hexdigest()
received = signature_header.replace("sha256=", "", 1)
return hmac.compare_digest(expected, received)
Billing is only applied after the entire requested pipeline succeeds.
If any stage fails, the job is marked as failed and no credits are charged for that API request.
Generated API DOCX files expire automatically. The current policy is:
410 Gone after expiry.| Code | HTTP Status | Meaning |
|---|---|---|
missing_template_id |
400 | template_id was not supplied. |
template_not_found |
404 | Template does not exist or is not accessible to the API company. |
insufficient_format_credit |
402 | Company does not have enough format credit. |
missing_file |
400 | No CV file was supplied. |
invalid_merge_options |
400 | merge_options was supplied but was not valid JSON. |
queue_failed |
500 | The API could not queue the formatting job. |
processing_failed |
Status endpoint / webhook | The asynchronous job failed during processing. |
document_not_ready |
409 | Download requested before the job completed. |
download_expired |
410 | Download requested after expiry. |
file_not_found |
404 | The generated file is no longer available. |
file_open_failed |
404 | The generated file exists in the job but could not be opened. |
POST /app/format_docx_api/job_id and status_urlstatus_url until status is complete or faileddownload_url if completePOST /app/format_docx_api/job_iddownload_url using the API key
The API can be called from any client capable of sending authenticated multipart/form-data requests.
The examples below show equivalent requests using cURL and Python.
curl -i -X POST "https://cvme.ai/app/format_docx_api/" \
-H "Authorization: Api-Key $CVME_API_KEY" \
-F "template_id=189" \
-F "target_language=English (British)" \
-F "file=@CVME_Test.pdf"
curl -i -X POST "https://cvme.ai/app/format_docx_api/" \
-H "Authorization: Api-Key $CVME_API_KEY" \
-F "template_id=189" \
-F "target_language=English (British)" \
-F "anonymise=true" \
-F "anonymise_scope=all" \
-F "file=@CVME_Test.pdf"
curl -i -X POST "https://cvme.ai/app/format_docx_api/" \
-H "Authorization: Api-Key $CVME_API_KEY" \
-F "template_id=189" \
-F "target_language=English (British)" \
-F "anonymise=true" \
-F "anonymise_scope=all" \
-F "note_merge_text=The candidate is immediately available, is open to hybrid working, and has recently completed additional cloud-focused project work. Add this information where appropriate without duplicating existing content." \
-F "optimise_text=Optimise this CV for a Senior Project Manager role requiring stakeholder management, delivery governance, budget control, vendor management, risk management, and experience delivering technology transformation projects." \
-F "notes_enabled=false" \
-F "candidate_mode=candidate" \
-F "abridge_cv=false" \
-F 'merge_options={"merge_summary":true,"merge_experience":true,"merge_skills_qualifications":true,"merge_cover_sheet":false}' \
-F "file=@CVME_Test.pdf"
curl -i "https://cvme.ai/app/format_docx_api/jobs/YOUR_JOB_ID/" \
-H "Authorization: Api-Key $CVME_API_KEY"
curl -L "https://cvme.ai/app/format_docx_api/jobs/YOUR_JOB_ID/download/" \
-H "Authorization: Api-Key $CVME_API_KEY" \
--output formatted-cv.docx
import json
import time
from pathlib import Path
import requests
API_KEY = "cvme_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
BASE_URL = "https://cvme.ai/"
CV_PATH = Path("CVME_Test.pdf")
def submit_cv():
url = f"{BASE_URL}/app/format_docx_api/"
headers = {
"Authorization": f"Api-Key {API_KEY}",
}
data = {
"template_id": "189",
"target_language": "English (British)",
"anonymise": "true",
"anonymise_scope": "all",
"note_merge_text": (
"The candidate is immediately available, is open to hybrid working, "
"and has recently completed additional cloud-focused project work. "
"Add this information where appropriate without duplicating existing content."
),
"optimise_text": (
"Optimise this CV for a Senior Project Manager role requiring "
"stakeholder management, delivery governance, budget control, "
"vendor management, risk management, and experience delivering "
"technology transformation projects."
),
"notes_enabled": "false",
"candidate_mode": "candidate",
"abridge_cv": "false",
"merge_options": json.dumps({
"merge_summary": True,
"merge_experience": True,
"merge_skills_qualifications": True,
"merge_cover_sheet": False,
}),
}
with CV_PATH.open("rb") as file_handle:
files = {
"file": (
CV_PATH.name,
file_handle,
"application/pdf",
),
}
response = requests.post(
url,
headers=headers,
data=data,
files=files,
timeout=60,
)
response.raise_for_status()
payload = response.json()
if not payload.get("success"):
raise RuntimeError(payload)
return payload
job = submit_cv()
print(json.dumps(job, indent=2))
def get_job_status(status_url):
headers = {
"Authorization": f"Api-Key {API_KEY}",
}
response = requests.get(
status_url,
headers=headers,
timeout=30,
)
response.raise_for_status()
payload = response.json()
if not payload.get("success"):
raise RuntimeError(payload)
return payload
def wait_for_job(status_url, poll_seconds=5, max_attempts=120):
for attempt in range(max_attempts):
status_payload = get_job_status(status_url)
status_value = status_payload.get("status")
if status_value == "complete":
return status_payload
if status_value == "failed":
raise RuntimeError(status_payload.get("error") or status_payload)
if status_payload.get("expired"):
raise RuntimeError("The job expired before it was downloaded.")
time.sleep(poll_seconds)
raise TimeoutError("Timed out waiting for CVME formatting job.")
complete_job = wait_for_job(
job["status_url"],
)
print(json.dumps(complete_job, indent=2))
def download_docx(download_url, output_path):
headers = {
"Authorization": f"Api-Key {API_KEY}",
}
response = requests.get(
download_url,
headers=headers,
timeout=120,
)
if response.headers.get("Content-Type", "").startswith("application/json"):
payload = response.json()
raise RuntimeError(payload)
response.raise_for_status()
Path(output_path).write_bytes(response.content)
return output_path
output_file = download_docx(
complete_job["download_url"],
"formatted-cv.docx",
)
print(f"Downloaded: {output_file}")
This example submits a CV file from a browser form, polls the job status, and downloads the final DOCX. Do not expose a shared production API key in public frontend code. For browser-based integrations, use a user-specific key, a Chrome extension storage flow, or proxy the request through your own backend.
<form id="cvmeApiForm">
<div class="mb-3">
<label for="apiKey" class="form-label">API key</label>
<input type="password" class="form-control" id="apiKey" required>
</div>
<div class="mb-3">
<label for="templateId" class="form-label">Template ID</label>
<input type="number" class="form-control" id="templateId" value="189" required>
</div>
<div class="mb-3">
<label for="cvFile" class="form-label">CV file</label>
<input type="file" class="form-control" id="cvFile" accept=".pdf,.doc,.docx,.txt" required>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="anonymise" checked>
<label class="form-check-label" for="anonymise">
Anonymise CV
</label>
</div>
<div class="mb-3">
<label for="noteMergeText" class="form-label">Note merge text</label>
<textarea class="form-control" id="noteMergeText" rows="3"></textarea>
</div>
<div class="mb-3">
<label for="optimiseText" class="form-label">Optimise text / job description</label>
<textarea class="form-control" id="optimiseText" rows="3"></textarea>
</div>
<button type="submit" class="btn btn-primary">
Submit to CVME
</button>
</form>
<pre id="cvmeApiOutput" class="mt-4"></pre>
const CVME_BASE_URL = "https://cvme.ai";
async function submitCvToCvme({
apiKey,
templateId,
file,
anonymise,
noteMergeText,
optimiseText
}) {
const formData = new FormData();
formData.append("template_id", String(templateId));
formData.append("target_language", "English (British)");
formData.append("anonymise", anonymise ? "true" : "false");
formData.append("anonymise_scope", "all");
formData.append("notes_enabled", "false");
formData.append("candidate_mode", "candidate");
formData.append("abridge_cv", "false");
formData.append(
"merge_options",
JSON.stringify({
merge_summary: true,
merge_experience: true,
merge_skills_qualifications: true,
merge_cover_sheet: false
})
);
if (noteMergeText) {
formData.append("note_merge_text", noteMergeText);
}
if (optimiseText) {
formData.append("optimise_text", optimiseText);
}
formData.append("file", file, file.name);
const response = await fetch(`${CVME_BASE_URL}/app/format_docx_api/`, {
method: "POST",
headers: {
Authorization: `Api-Key ${apiKey}`
},
body: formData
});
const payload = await response.json();
if (!response.ok || payload.success === false) {
throw new Error(
payload?.error?.message || "CVME API request failed."
);
}
return payload;
}
async function getCvmeJobStatus(apiKey, statusUrl) {
const response = await fetch(statusUrl, {
method: "GET",
headers: {
Authorization: `Api-Key ${apiKey}`
}
});
const payload = await response.json();
if (!response.ok || payload.success === false) {
throw new Error(
payload?.error?.message || "Could not fetch CVME job status."
);
}
return payload;
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function waitForCvmeJob(apiKey, statusUrl, {
pollMs = 5000,
maxAttempts = 120
} = {}) {
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
const statusPayload = await getCvmeJobStatus(apiKey, statusUrl);
if (statusPayload.status === "complete") {
return statusPayload;
}
if (statusPayload.status === "failed") {
throw new Error(
statusPayload?.error?.message || "CVME formatting job failed."
);
}
if (statusPayload.expired) {
throw new Error("The CVME job expired before it was downloaded.");
}
await sleep(pollMs);
}
throw new Error("Timed out waiting for CVME formatting job.");
}
async function downloadCvmeDocx(apiKey, downloadUrl, filename = "formatted-cv.docx") {
const response = await fetch(downloadUrl, {
method: "GET",
headers: {
Authorization: `Api-Key ${apiKey}`
}
});
const contentType = response.headers.get("Content-Type") || "";
if (contentType.includes("application/json")) {
const payload = await response.json();
throw new Error(
payload?.error?.message || "CVME download failed."
);
}
if (!response.ok) {
throw new Error("CVME download failed.");
}
const blob = await response.blob();
const objectUrl = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = objectUrl;
link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(objectUrl);
}
This example uses Node.js 18+ with the built-in fetch, FormData, and Blob APIs.
import { readFile, writeFile } from "node:fs/promises";
import { basename } from "node:path";
const API_KEY = "cvme_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
const BASE_URL = "https://cvme.ai";
const CV_PATH = "CVME_Test.pdf";
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function submitCv() {
const fileBuffer = await readFile(CV_PATH);
const formData = new FormData();
formData.append("template_id", "189");
formData.append("target_language", "English (British)");
formData.append("anonymise", "true");
formData.append("anonymise_scope", "all");
formData.append("notes_enabled", "false");
formData.append("candidate_mode", "candidate");
formData.append("abridge_cv", "false");
formData.append(
"merge_options",
JSON.stringify({
merge_summary: true,
merge_experience: true,
merge_skills_qualifications: true,
merge_cover_sheet: false
})
);
formData.append(
"note_merge_text",
"The candidate is immediately available and is open to hybrid working."
);
formData.append(
"optimise_text",
"Optimise this CV for a Senior Project Manager role."
);
formData.append(
"file",
new Blob([fileBuffer], { type: "application/pdf" }),
basename(CV_PATH)
);
const response = await fetch(`${BASE_URL}/app/format_docx_api/`, {
method: "POST",
headers: {
Authorization: `Api-Key ${API_KEY}`
},
body: formData
});
const payload = await response.json();
if (!response.ok || payload.success === false) {
throw new Error(
payload?.error?.message || "CVME API request failed."
);
}
return payload;
}
async function getJobStatus(statusUrl) {
const response = await fetch(statusUrl, {
method: "GET",
headers: {
Authorization: `Api-Key ${API_KEY}`
}
});
const payload = await response.json();
if (!response.ok || payload.success === false) {
throw new Error(
payload?.error?.message || "Could not fetch CVME job status."
);
}
return payload;
}
async function waitForJob(statusUrl) {
for (let attempt = 0; attempt < 120; attempt += 1) {
const payload = await getJobStatus(statusUrl);
if (payload.status === "complete") {
return payload;
}
if (payload.status === "failed") {
throw new Error(
payload?.error?.message || "CVME formatting job failed."
);
}
if (payload.expired) {
throw new Error("The CVME job expired before it was downloaded.");
}
await sleep(5000);
}
throw new Error("Timed out waiting for CVME formatting job.");
}
async function downloadDocx(downloadUrl, outputPath) {
const response = await fetch(downloadUrl, {
method: "GET",
headers: {
Authorization: `Api-Key ${API_KEY}`
}
});
const contentType = response.headers.get("Content-Type") || "";
if (contentType.includes("application/json")) {
const payload = await response.json();
throw new Error(
payload?.error?.message || "CVME download failed."
);
}
if (!response.ok) {
throw new Error("CVME download failed.");
}
const arrayBuffer = await response.arrayBuffer();
await writeFile(
outputPath,
Buffer.from(arrayBuffer)
);
return outputPath;
}
const job = await submitCv();
console.log("Queued job:", job);
const completedJob = await waitForJob(
job.status_url
);
console.log("Completed job:", completedJob);
const outputPath = await downloadDocx(
completedJob.download_url,
completedJob.filename || "formatted-cv.docx"
);
console.log(`Downloaded: ${outputPath}`);