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

Source: https://www.remakecv.com/help/api-reference/recipes/process-and-download
Last updated: 2026-08-21

---
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](https://www.remakecv.com/help/api-reference/authentication.md)
- **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 title="process-cv.mjs"
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 |

> **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`](https://www.remakecv.com/help/api-reference/endpoints/get-cv-file.md) 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](https://www.remakecv.com/help/api-reference/rate-limits.md).

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