Loading...

CVME Format DOCX API

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.

Supported pipeline stages

  • Format CV into a CVME object
  • Optionally anonymise employer and candidate-identifying information
  • Optionally merge notes or supporting information
  • Optionally optimise the CV against a job description or target brief
  • Render the final DOCX
  • Send a webhook notification when complete or failed
  • Provide an authenticated download link with automatic expiry

Authentication

All API requests must include a company API key.

Authorization header

Authorization: Api-Key YOUR_API_KEY

Alternative header

X-CVME-API-Key: YOUR_API_KEY

Example

export CVME_API_KEY="cvme_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Where to find your API key: API credentials are created and managed from your CVME dashboard under Account Settings in the API / Integrations section. From there you can create an API integration, copy your API key, set your webhook URL, view your webhook secret, and rotate credentials when required.

Standard Response Format

Success response

{
  "success": true,
  "job_id": "a2bbc740-758c-450e-b2ec-1f49ec3d9d06",
  "status": "queued"
}

Error response

{
  "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 DOCX Formatting Job

POST /app/format_docx_api/

Submit a CV file using multipart/form-data. The response returns a queued job with status and download URLs.

Required fields

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.

Optional fields

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.

Supported target languages

The target_language field accepts the following values. The default is English (British).

Amharic
Arabic
Bengali
Bosnian
Bulgarian
Chinese
Chinese (Simplified)
Chinese (Traditional)
Croatian
Czech
Danish
Dutch
English (American)
English (British)
Estonian
Finnish
French
German
Greek
Hausa
Hebrew
Hindi
Hungarian
Icelandic
Igbo
Indonesian
Italian
Japanese
Korean
Latvian
Lithuanian
Malay
Marathi
Norwegian
Persian (Farsi)
Polish
Portuguese
Romanian
Russian
Serbian
Slovak
Slovenian
Spanish (Spain)
Spanish (Mexico)
Spanish (Argentina)
Spanish (United States)
Swahili
Swedish
Tamil
Telugu
Thai
Turkish
Ukrainian
Urdu
Vietnamese
Xhosa
Yoruba
Zulu

Merge options

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.
Important: merge_options must be sent as a valid JSON string inside the multipart form request. Invalid JSON returns invalid_merge_options.

Default merge behaviour

{
          "merge_summary": true,
          "merge_experience": true,
          "merge_skills_qualifications": true,
          "merge_cover_sheet": true
        }

Example: only update summary and notes

{
          "merge_summary": true,
          "merge_experience": false,
          "merge_skills_qualifications": false,
          "merge_cover_sheet": false
        }

Example: merge supporting experience but do not touch the cover sheet

{
          "merge_summary": true,
          "merge_experience": true,
          "merge_skills_qualifications": true,
          "merge_cover_sheet": false
        }

cURL example

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"

Submit Response

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
  }
}

Check Job Status

GET /app/format_docx_api/jobs/{job_id}/

Example request

curl -i "https://cvme.ai/app/format_docx_api/jobs/a2bbc740-758c-450e-b2ec-1f49ec3d9d06/" \
  -H "Authorization: Api-Key $CVME_API_KEY"

Queued response

{
  "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": ""
}

Complete response

{
  "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/"
}

Failed response

{
  "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."
  }
}

Download Formatted DOCX

GET /app/format_docx_api/jobs/{job_id}/download/

Example request

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

Success

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

Download Error Responses

Document not ready

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"
}

Download expired

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"
}

File missing

HTTP 404 Not Found

{
  "success": false,
  "error": {
    "code": "file_not_found",
    "message": "Generated file not found."
  },
  "job_id": "a2bbc740-758c-450e-b2ec-1f49ec3d9d06"
}

List Available Templates

GET /app/format_docx_api/templates/

Example request

curl -i "https://cvme.ai/app/format_docx_api/templates/" \
        -H "Authorization: Api-Key $CVME_API_KEY"

Example response

{
          "success": true,
          "count": 2,
          "templates": [
            {
              "id": 189,
              "name": "Standard CV Template"
            },
            {
              "id": 201,
              "name": "Public Template"
            }
          ]
        }

Webhooks

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.

Events

format_docx.complete
format_docx.failed

Webhook headers

Content-Type: application/json
X-CVME-Event: format_docx.complete
X-CVME-Timestamp: 1788439580
X-CVME-Signature: sha256=...

Webhook signature verification

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.

Complete webhook payload

{
  "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": ""
}

Failed webhook payload

{
  "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."
}

Webhook Verification Example

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 Behaviour

Billing is only applied after the entire requested pipeline succeeds.

  • CV formatting succeeds
  • Anonymisation succeeds, if enabled
  • Note merge succeeds, if supplied
  • Optimisation succeeds, if supplied
  • Final DOCX rendering succeeds
  • Output file is attached to the job

If any stage fails, the job is marked as failed and no credits are charged for that API request.

File Expiry and Storage

Generated API DOCX files expire automatically. The current policy is:

  • DOCX files expire after 1 day.
  • The download endpoint returns 410 Gone after expiry.
  • A scheduled cleanup task removes expired DOCX files from storage.
  • Format job records can be retained for audit and history.

Error Codes

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.

Recommended Client Flow

Polling flow

  1. Submit CV with POST /app/format_docx_api/
  2. Store job_id and status_url
  3. Poll status_url until status is complete or failed
  4. Download from download_url if complete
  5. Stop if failed or expired

Webhook flow

  1. Submit CV with POST /app/format_docx_api/
  2. Store job_id
  3. Wait for webhook
  4. Verify webhook signature
  5. Download from download_url using the API key

Code Examples

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.

Format only

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"

Format with anonymisation

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"

Format, anonymise, note merge, and optimise

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"

Check status

curl -i "https://cvme.ai/app/format_docx_api/jobs/YOUR_JOB_ID/" \
      -H "Authorization: Api-Key $CVME_API_KEY"

Download DOCX

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

Submit formatting job

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))

Poll status until complete

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))

Download the final DOCX

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}")

Browser JavaScript example

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.

Example HTML form

<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>

Submit formatting job

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;
    }

Poll status until complete

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.");
    }

Download final DOCX

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);
    }

Node.js example

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}`);