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,.docor.docx
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
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
| Field | Why you should act on it |
|---|---|
processing_method | image means OCR ran — flag for human review |
id | Persist this. It is how you get a fresh download URL later |
candidate_name | null means extraction could not find a name — worth flagging |
latest_company / latest_role | The fields most worth writing into your own system |
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.
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:
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
Last updated . Still stuck? Email support@remakecv.com or book a call.