Pre-Black Friday: 20% off, for as long as you stayEARLYBFon Solo and AgencyEnds Nov 28Start free

How-to guide

How to Use Claude Code for Salesforce Apex Development (Build an Enrichment Callout)

Have Claude Code write, test and deploy a Queueable Apex class that enriches new Leads from LeadOcean, with the real code, tests and a checked count.

Get your free API key →Free to start. No credit card. 1,000 records to spend whenever you like.

TL;DR

  • Claude Code writes Apex well when you give it a spec, a test target and the sf CLI. This guide builds one real class: a Queueable that enriches a new Lead through LeadOcean.
  • It takes about 40 minutes the first time. Testing on a scratch org spends about 2 of your 1,000 free records.
  • Claude Code drafts. You still read the diff, run the tests and review governor limits before anything reaches production.

Prerequisites

  • A Salesforce scratch org or developer sandbox. Never start in production.
  • The Salesforce CLI (sf), authorised to that org, and an SFDX project.
  • Claude Code installed in the project folder.
  • A LeadOcean account from app.leadocean.io. The free plan has no card and 1,000 records, one-off.
  • A LeadOcean API key exported as $LEADOCEAN_API_KEY for the REST checks.
bash
export LEADOCEAN_API_KEY="paste-your-key-here"
sf org list

The examples use acme.com and Jane Doe as placeholders.

Step 1: Give Claude Code the rules in CLAUDE.md

Claude Code reads a CLAUDE.md file in the project root at the start of a session. Put your Apex standards there once, so every class follows them.

markdown
Apex rules for this repo:
- API version 62.0. One trigger per object, logic in handler classes.
- No SOQL or DML inside loops. Bulkify everything.
- Callouts only through a Named Credential called LeadOcean. Never a literal key.
- Triggers cannot call out directly. Use Queueable with Database.AllowsCallouts.
- Every class needs a test with HttpCalloutMock and 90%+ coverage.
- Run `sf apex run test --code-coverage --result-format human` before saying done.
- Deploy only to the scratch org alias `dev`. Never to production.

Do not paste your key into this file or into a prompt. The Named Credential holds it.

Step 2: Size the problem for free

Before writing a line, check what the callout will return for. A count costs 0 records. Connect the MCP server once, then ask.

bash
claude mcp add --transport http --scope user leadocean https://api.leadocean.io/mcp
claude mcp login leadocean
text
Use the LeadOcean MCP server only. Call leadocean_count_leads with
title ["Salesforce Developer"] and country ["US"]. Report total, totalAll and
distinctCompanies, and say which email statuses the total counted. Do not pull anyone.

The server exposes 13 tools. Seven never spend records. The full walkthrough is on Connect LeadOcean to Claude Code.

Step 3: Have Claude Code write the Queueable

Give it a spec, not a vague ask. Name the endpoint, the input and what to write back.

text
Create force-app/main/default/classes/LeadEnrichQueueable.cls.
- Implements Queueable, Database.AllowsCallouts. Takes a List<Id> of Leads.
- For each Lead with an email, POST to callout:LeadOcean/v1/people/enrich
  with body {"email": <email>}. One call per Lead.
- On 200, write data.contact_data.contact_current_experiences[0].job_title
  into Lead.Title only if Title is blank.
- On 404, do nothing. On 429, re-enqueue once. Log other codes with System.debug.
- Process at most 50 Leads per job. Add a LeadTrigger handler that enqueues it.
Then write the test class with an HttpCalloutMock. Do not deploy yet.

Expect something close to this. Read every line before you accept it.

apex
public with sharing class LeadEnrichQueueable implements Queueable, Database.AllowsCallouts {
    private List<Id> leadIds;
    public LeadEnrichQueueable(List<Id> leadIds) { this.leadIds = leadIds; }

    public void execute(QueueableContext ctx) {
        List<Lead> leads = [SELECT Id, Email, Title FROM Lead WHERE Id IN :leadIds];
        List<Lead> toUpdate = new List<Lead>();
        for (Lead l : leads) {
            if (String.isBlank(l.Email)) continue;
            HttpRequest req = new HttpRequest();
            req.setEndpoint('callout:LeadOcean/v1/people/enrich');
            req.setMethod('POST');
            req.setHeader('content-type', 'application/json');
            req.setBody(JSON.serialize(new Map<String, String>{ 'email' => l.Email }));
            HttpResponse res = new Http().send(req);
            if (res.getStatusCode() == 200 && String.isBlank(l.Title)) {
                Map<String, Object> body = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
                Map<String, Object> data = (Map<String, Object>) body.get('data');
                Map<String, Object> contact = (Map<String, Object>) data?.get('contact_data');
                List<Object> roles = (List<Object>) contact?.get('contact_current_experiences');
                if (roles != null && !roles.isEmpty()) {
                    Map<String, Object> role = (Map<String, Object>) roles[0];
                    if (role.get('job_title') != null) {
                        l.Title = (String) role.get('job_title');
                        toUpdate.add(l);
                    }
                }
            }
        }
        if (!toUpdate.isEmpty()) update toUpdate;
    }
}

The current role sits in contact_data.contact_current_experiences[], per the enrich docs. Step 5 confirms it against a live response.

Step 4: Set up the Named Credential and deploy

Create a Named Credential called LeadOcean that points at https://api.leadocean.io. A named credential holds the endpoint URL and its authentication in one definition, and Apex reaches it as callout:LeadOcean/... (Salesforce Apex docs, October 2026). LeadOcean expects the key in an x-api-key header. Configure that header on the credential in Setup, following Salesforce Help for your org, so the key never appears in Apex.

Then let Claude Code deploy and test against the scratch org.

bash
sf project deploy start --target-org dev
sf apex run test --target-org dev --code-coverage --result-format human --wait 10

Paste failures back into the session. Claude Code is good at fixing a compile error or a failing assertion from the output. It cannot see your org's validation rules, so tell it about them.

Step 5: Check the real response shape

Before trusting the field path, call the endpoint once yourself. This spends 1 record if the person is found, and nothing on a 404.

bash
curl -s https://api.leadocean.io/v1/people/enrich \
  -H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
  -d '{"email": "jane.doe@acme.com"}' | jq 'keys, (.data | keys)'

Paste the output into the session and ask Claude Code to correct the parser. Enrich takes exactly one of linkedin_url, email, phone or person_id. Since October 2026 it also returns career history under resume_data.

What you get

The count in Step 2 is a real result. We ran it through the free leadocean_count_leads tool on 2026-10-21 (trimmed).

json
{
  "total": 3563,
  "totalIsExact": true,
  "totalAll": 13143,
  "distinctCompanies": 1788,
  "credits": 0,
  "defaults": "Counted only addresses worth sending to (emailStatus: verified or catch_all_valid or catch_all)",
  "unsupportedFilters": []
}

That is 3,563 mailable people titled Salesforce Developer in the US, across 1,788 companies. Without an email filter the same filters match 13,143. Your number will differ as the data changes.

The enrich response, as the API reference describes it, with placeholder values (response shape from the API reference):

json
{
  "success": true,
  "data": {
    "profile_data": { "profile_full_name": "Jane Doe" },
    "contact_data": {
      "has_email": true,
      "email_status": "verified",
      "contact_current_experiences": [
        { "company_name": "Acme", "company_domain": "acme.com", "job_title": "Salesforce Developer" }
      ]
    },
    "resume_data": { "experiences": [] }
  },
  "meta": { "credits": 1 }
}

The test run enriches one Lead, which is 1 record. A Lead we hold nothing for is a 404 and costs nothing.

Troubleshooting

ErrorCauseFix
401Missing or wrong key in the Named Credential headerRe-copy the key from app.leadocean.io and update the credential.
429Above your plan's rate limit: 5 requests a second on Free, 200 on AgencyEnqueue smaller batches and honour Retry-After. Claude Code can add the single retry.
402The 1,000 free records are spentCheck GET /v1/account or move to Solo.
404 on enrichWe hold nothing for that emailSkip the Lead. A 404 costs nothing.
Count of 0 or 400 on a filterA value outside the catalogueCall leadocean_list_enum_values and pass the exact string.
Callout not allowed from a triggerThe class makes the HTTP call inside the trigger contextKeep the call in the Queueable, as in Step 3.

FAQ

Can Claude Code deploy to production?

It can run any sf command you allow, so do not allow that one. Keep a scratch org alias in CLAUDE.md and deploy to production through your normal pipeline after review.

Does Claude Code know Apex governor limits?

It knows the common ones, such as no SOQL in loops. It does not know your org's data volume. Review batch sizes and run the tests with realistic data.

How many records does the enrichment cost at scale?

One per Lead found. Misses cost nothing. Paid plans run on a fair-use pace, not credits, so there is no per-record invoice. See pricing.

Does LeadOcean have a Salesforce package?

No. LeadOcean returns data over REST and MCP. You write the Apex, as above, or sync from a tool such as Clay.

More Salesforce recipes: workflow automation, skills for admins and MCP setup. All recipes live in the blog hub.

Enrich your Salesforce Leads from Apex, free

Free to start. No credit card. 1,000 records to spend whenever you like.

Get your free API key →