RemakeCV

Recipe: process a CV and store the result

A complete worked example — authenticate, upload a CV for parsing, read the extracted candidate details, and retrieve the stored source file, with retry and error handling.

This recipe walks through a full RemakeCV integration: fetch the template list, POST a CV to /cvs/process, read the extracted candidate name, employer and role, and retrieve the stored source file. It includes rate-limit backoff and the error handling a production integration needs.

What you need

  • An API key — a company administrator creates one in Company Settings → Public API. See authentication
  • CV storage enabled for your company. Without it every call spends a credit and returns 400 storage_required
  • The email address of the consultant the work is on behalf of
  • A CV file in .pdf, .doc or .docx
Warning:

This endpoint parses and stores a CV. It does not render one into your template — download_url returns the file you uploaded, converted to PDF. Plan your integration around the extracted fields, not around a formatted document.

The complete flow

javascript
import fs from 'node:fs/promises';

const BASE = 'https://app.remakecv.com/api/public/v1';
const KEY = process.env.REMAKECV_API_KEY;
const ACTING_USER = 'consultant@agency.com';

const auth = { Authorization: `Bearer ${KEY}` };

/** Retries only on 429 and 5xx; 4xx errors are the caller's problem to fix. */
async function request(path, init = {}, attempts = 4) {
  for (let attempt = 0; attempt < attempts; attempt += 1) {
    const response = await fetch(`${BASE}${path}`, {
      ...init,
      headers: { ...auth, ...init.headers },
    });

    if (response.ok) return response.json();

    if (response.status !== 429 && response.status < 500) {
      const { error } = await response.json();
      throw new Error(`${error.code}: ${error.message} (request_id ${error.request_id})`);
    }

    const wait = 2 ** attempt * 1000 + Math.random() * 500;
    await new Promise((r) => setTimeout(r, wait));
  }
  throw new Error(`Gave up after ${attempts} attempts: ${path}`);
}

async function findTemplate(name) {
  const { data } = await request(
    `/templates?acting_user_email=${encodeURIComponent(ACTING_USER)}`
  );
  return data.find((t) => t.name === name) ?? data.find((t) => t.is_default);
}

async function processCv(filePath, templateId) {
  const form = new FormData();
  form.append('file', new Blob([await fs.readFile(filePath)]), filePath);
  form.append('acting_user_email', ACTING_USER);
  if (templateId) form.append('template_id', String(templateId));

  const { data } = await request('/cvs/process', { method: 'POST', body: form });
  return data;
}

const template = await findTemplate('Agency Standard');
const cv = await processCv('./candidate-cv.pdf', template?.id);

console.log(`${cv.candidate_name}${cv.latest_role} at ${cv.latest_company}`);

// OCR was needed, so this result is worth a closer human review.
if (cv.processing_method === 'image') {
  console.warn('Scanned CV — verify extracted fields before sending.');
}

// download_url points at the SOURCE CV as a PDF, and expires after an hour.
const file = await fetch(cv.download_url);
await fs.writeFile(`./source-${cv.id}.pdf`, Buffer.from(await file.arrayBuffer()));

What the response tells you

FieldWhy you should act on it
processing_methodimage means OCR ran — flag for human review
idPersist this. It is how you get a fresh download URL later
candidate_namenull means extraction could not find a name — worth flagging
latest_company / latest_roleThe fields most worth writing into your own system
Warning:

download_url expires after one hour, so a job that processes now and downloads on a nightly schedule will fail. Store id and fetch a fresh URL from GET /cvs/{cvId}/file when you actually need the file.

Tip:

The retry helper above retries on 5xx. Be careful: process_failed and storage_failed can occur after a credit has been deducted, so a blind retry loop can spend several credits on one CV. Cap the attempts, as this example does.

Processing a batch

At 30 requests per minute, pace deliberately rather than firing everything at once:

javascript
async function processBatch(paths, templateId) {
  const results = [];
  for (const path of paths) {
    results.push({ path, cv: await processCv(path, templateId) });
    // Roughly 30/min, leaving headroom for the template lookup and retries.
    await new Promise((r) => setTimeout(r, 2100));
  }
  return results;
}

Track which files succeeded so a failed run resumes rather than restarting — reprocessing costs credits. See rate limits.

Frequently asked questions

Can I skip the template lookup?
Yes, but pass template_id explicitly if it matters — the fallback when you omit it is a separate public-API setting, not the is_default template.
Does this give me a CV formatted into our template?
No. The public API parses and stores CVs; download_url returns the source file as a PDF. Template rendering is only available in the web app.
Should I download immediately?
Only if you need the file now. download_url lasts an hour; store the CV id and fetch a fresh URL from /cvs/{cvId}/file when required.

Related articles

Was this page helpful?

Last updated . Still stuck? Email support@remakecv.com or book a call.