ResumeJSON

Resume parser for Salesforce: parse CVs into records with Apex

A resume parser for Salesforce is a small piece of Apex that takes a CV attached to a record, sends the file to a parsing API, and writes the fields it gets back onto that record. Salesforce does not parse resumes on its own. If you run a recruiting app from AppExchange, check first whether it already parses on upload, because many do. If your candidates live in standard or custom objects you built yourself, the parse step is yours to add, and it is about a hundred lines of code.

This guide is for the Salesforce developer or admin asked to stop recruiters retyping CVs into a Contact or a custom Candidate__c object. It covers where the file comes from, how to call the parser, which governor limits decide the design, and how to keep a bad file from turning into a blank record.

How the pieces fit

Every working setup has the same four parts:

  1. The file. A CV uploaded to a record lands as a ContentVersion, and its bytes are in the VersionData field.
  2. The trigger. Something notices the upload: a trigger on ContentDocumentLink, a record-triggered Flow, or a button a recruiter presses.
  3. The callout. Apex sends the bytes to a resume parsing API over HTTPS and reads JSON back.
  4. The write. Apex maps the parsed fields onto the candidate record and saves it.

The callout is the part that needs care, because Salesforce limits what a callout may do and when.

The Salesforce limits that decide the design

These figures are from Salesforce's own Apex documentation as of October 2026.

LimitSynchronous ApexAsynchronous Apex
Callout request or response size6 MB12 MB
Total heap size10 MB25 MB
Callouts per transaction100100
Cumulative callout timeout per transaction120 seconds120 seconds
Default timeout of one callout10 seconds10 seconds

Three rules follow from that table and from the callout documentation.

You can't make a callout when there are pending operations in the same transaction.

DML statements count as pending operations. So the trigger hands the work to a Queueable job, and the job makes the callout before it writes.

A CV larger than the callout ceiling cannot be sent from Apex at all, even if the parser itself accepts it. Check ContentSize first and route an oversized file to a person.

Step 1: let Salesforce reach the parser

Salesforce blocks callouts to hosts it has not been told about. Create a Named Credential for the parser's host, so the URL and the API key live in setup instead of in code. With ResumeJSON the host is resumejson-resume-cv-parser-api.p.rapidapi.com and the key comes from the RapidAPI listing, sent in the x-rapidapi-key header. A Named Credential with a custom header, or an External Credential holding the key, keeps it out of the Apex class and out of source control.

If you only want to see what comes back before you build anything, the free parser takes one file with no signup and shows the same JSON.

Step 2: send the file as the raw body

Apex has no convenient multipart builder, and you do not need one. ResumeJSON accepts the file bytes as the raw request body and detects PDF, DOCX and image types from the bytes themselves, so setBodyAsBlob is the whole upload:

public class ResumeParseJob implements Queueable, Database.AllowsCallouts {
    private final Id versionId;
    private final Id candidateId;

    public ResumeParseJob(Id versionId, Id candidateId) {
        this.versionId = versionId;
        this.candidateId = candidateId;
    }

    public void execute(QueueableContext ctx) {
        ContentVersion cv = [
            SELECT VersionData, FileExtension, ContentSize
            FROM ContentVersion WHERE Id = :versionId
        ];
        Candidate__c cand = new Candidate__c(Id = candidateId);

        if (cv.ContentSize > 11 * 1024 * 1024) {
            cand.Parse_Status__c = 'Too large for Apex';
            update cand;
            return;
        }

        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:ResumeJSON/v1/parse');
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/octet-stream');
        req.setBodyAsBlob(cv.VersionData);
        req.setTimeout(60000);

        HttpResponse res = new Http().send(req);
        // the callout is done, so writing is allowed from here on
        ResumeMapper.apply(cand, res);
        update cand;
    }
}

The ceiling in the size check sits a little under 12 MB because the request carries headers as well as the file. A trigger on ContentDocumentLink finds the latest ContentVersion for each new link whose LinkedEntityId is a candidate, and enqueues one job per file with System.enqueueJob.

Step 3: map the JSON onto the record

The response is JSON with a resume object in a fixed, documented shape, so the mapping is written once:

Parsed fieldExample Salesforce fieldNote
resume.basics.full_nameNamenull when the CV states no name
resume.basics.emailEmail__cUse it for duplicate checks
resume.basics.phonePhone__cAs written on the CV
resume.work[0].titleCurrent_Title__cCheck is_current before calling it current
resume.skillsSkills__c (long text or multi-select)An array of strings
resume.total_years_experienceYears_Experience__cA number, or null

Parse it with JSON.deserializeUntyped, or generate an Apex class from a sample response and use JSON.deserialize for typed access. Either way, treat null as "the CV does not say" and leave the Salesforce field empty instead of writing a placeholder. A recruiter filtering on years of experience should never see a zero that means "unknown".

Store the whole JSON in a long text field as well. The fields you map today are the ones you search on; the rest of the CV, such as education, every past role and languages, stays available when someone asks for it next quarter.

Step 4: handle the failures where a recruiter will see them

Some uploads are not CVs: a cover letter on its own, a portfolio, a blank page. ResumeJSON's refusals are specific, so each one becomes a status the recruiter can act on:

StatusError codeWhat it meansSet Parse_Status__c to
413too_largeThe file is over the size limitToo large
415unsupported_typeNot a PDF, DOCX, text or imageUnsupported file
422unreadable_fileThe file could not be readUnreadable, check by hand
422not_a_resumeThe document is not a CVNot a CV
200noneParsedParsed

Never retry a 4xx. It spells the same answer again, more slowly. Retry only what is transient, such as a callout timeout, and do it by enqueueing a fresh job with a counter so a stuck file stops after two or three tries. Put Parse_Status__c on the record page and in a list view, so a failed parse is something a recruiter sees beside the candidate, rather than a record that quietly looks like an empty CV.

Step 5: test it without calling out

Apex tests cannot make real callouts. Implement HttpCalloutMock, return a saved sample response for the happy path and one response per error code, and assert what lands on the record in each case. The refusal branches are where a quiet bug hides: a test that only covers the 200 will pass against code that writes a blank candidate for every cover letter.

Choosing how to parse inside Salesforce

A recruiting app that parsesYour own Apex and a parsing APITyping it in
SetupInstall and configureAbout a hundred lines plus testsNone
Works with your own objectsOnly the app'sYesYes
File typesWhatever the app supportsWhatever the API supportsAnything a person can read
Running costThe app's licenceEach parse, on the API's planRecruiter time
Best whenYou want a whole recruiting productYour data model is already builtA handful of CVs a week

If you are still choosing a parsing API, the CV parser API comparison covers what each option returns and costs. If you would rather build this outside Salesforce and push the results in, the n8n resume parser guide does the same job as a workflow.

When the manual way is enough

Skip the code when:

In those cases a required file upload and a short checklist on the record page are enough. The point to automate is when typing records takes longer than finding candidates in them saves. Once it does, the candidate database guide covers which fields are worth keeping, and the parse step above fills them.

Try one CV first

Before writing the Queueable, run one real CV from your org through the free parser and look at the JSON. It shows which fields your CVs actually carry, and that is the mapping table for Step 3. The API docs list every field and the plans.

All articles