Skip to content
Last updated

Salesforce Lead Enrichment

Using the Lusha v3 Search & Enrich API

Description

This guide walks through building a record-triggered Salesforce Flow that automatically enriches every newly created Lead using the Lusha v3 API. When a Lead is created, the Flow sends whatever identifiers are available to Lusha, receives the matching contact profile, and writes the returned data back onto the Lead record.

The integration is entirely declarative. No Apex is written or deployed at any point.

Note: v2 endpoints stop responding in November 2026. Any integration still calling a /v2/ URL will break on that date.

Prerequisites

1. Get your Lusha API Key

Visit dashboard.lusha.com → API Hub → Manage API Keys → + Create new Key. Copy it — you'll paste it once below. Auth is via an api_key HTTP header (not Bearer).

2. Salesforce Named and External Credential Setup

StepConfiguration
Setup → Named Credentials → External Credentials → NewLabel/Name: Lusha v3 / Lusha_v3. Auth Protocol: Custom. Save.
On that record → Principals → NewParameter Name Admin, Sequence 1, Identity Type Named Principal. Under Authentication Parameters → Add: Name LushaApiKey, Value = your API key. Save.
Same record → Custom Headers → NewName api_key, Value {!$Credential.Lusha_v3.LushaApiKey}, Sequence 1.
Setup → Named Credentials → Named Credentials tab → NewLabel/Name: Lusha v3 / Lusha_v3. URL: https://api.lusha.com. External Credential: the one above. Uncheck Generate Authorization Header. Check Allow Formulas in HTTP Header. Save.

Access to the Principal is granted from the Permission Set side only: the External Credential page itself has no working "grant access" control, despite appearing to.

3. Create Permission Sets

Creating the credential is not sufficient on its own. Salesforce requires an explicit grant before any user can invoke it.

1. Create a Permission Set

  • Label: Lusha API Access
  • Open the permission set and click External Credential Principal Access
  • Click Edit, move your Lusha external credential principal from Available to Enabled, and click Save

2. Assign the Permission Set

Assign it to every user who will create Leads — not only to yourself.

This is the most common cause of a silent 401. Record-triggered Flows run in system context, but the External Credential principal still resolves against the running user. If the user who created the Lead lacks this permission set, the callout fails with 401 and — because it runs asynchronously — produces no visible error anywhere in the UI.

Flow Configuration

There are 4 core sets of elements to configure within your new Flow. They are organized below to help you visualize the complete structure:

PartPurposeElements
1. Trigger and calloutAssemble the request and call LushaBuild Lusha Request, Enrich Contact via Lusha, Each Result Loop
2. Set EmailSelect the first work emailError Capture Attempt, Email Loop, Email Capture Attempt
3. Set PhonesSelect mobile and other numbersPhone Loop, Phone Capture Attempt
4. Update LeadCommit the recordFound a Match Attempt, Update Record

To build this flow, you must begin with a Record-Triggered Flow, and the HTTP Callout Action element, otherwise you will encounter inconsistencies when configuring your Apex Defined Variables items. Once created, you will build and connect all remaining steps around the Callout Action.

Salesforce Lead enrichment Flow structure

1. Begin by creating a new Flow

  • Within the Salesforce Setup menu, search for Flows.
  • Click the New Flow button.
  • Choose your automation type. We recommend Record-Triggered Flow for automatic enrichment on creation, or Screen Flow if you would prefer to trigger enrichment manually via a button.

2. Configure the start of your Flow

  • Select an Object: Lead
  • Trigger the flow when: A record is created
  • Set Entry Conditions:
    • Condition Requirements: none [see Appendix C if you'd like requirements]
  • Optimize the Flow for: Actions and Related Records
  • Enable Add Asynchronous Path

For the rest of this guide, remember these two important details:

Connect everything to the asynchronous path only. All following steps connect to the Run Asynchronously path. Do not connect any step to the Run Immediately path. Salesforce forbids HTTP callouts in the same transaction as the triggering DML, so a callout placed on the immediate path fails at activation.
Scheduled Paths will always read "2". This is expected. The count includes the built-in Run Immediately path plus your asynchronous path. Run Immediately cannot be deleted, so simply leave it unconnected.

3. Configure the Action Element (HTTP Callout)

Add an Action element

  • Click the Create HTTP Callout button
    • Name: Lushav3
    • Named Credential: Lusha v3
  • Click Next
    • Label: Enrich
    • Method: POST
    • Path: /v3/contacts/search-and-enrich
  • Click Next
    • Sample JSON Request: [see Appendix A]
  • Click Review, then click Next
  • Choose Use Example Response
    • Sample JSON Response: [see Appendix B]
  • Click Review
  • Within the Data Structure section, change the following elements to String type:
    • jobTitle.startDate
    • emails.updateDate
    • phones.updateDate
    • results.updateDate
  • Click Save
  • New Action Label: "Enrich Contact via Lusha Action"
  • API Name: "Lusha"
  • Set the Request Body:
    • Click + New Resource
    • API Name: varRequestBody
    • Click Done
  • Click Done, then connect Build Request Body to Lusha Enrich

4. Create the Build Request Step

Add an Assignment element ABOVE the Action Step

  • Label: Build Contact
  • Map the following:
VariableOperatorValue
varRequestItem > firstNameEqualsTriggering Record > First Name
varRequestItem > lastNameEqualsTriggering Record > Last Name
varRequestItem > companyNameEqualsTriggering Record > Company
varRequestItem > companyDomainEquals{!formCompanyDomain} [see Appendix D]
varRequestItem > emailEqualsTriggering Record > Email
varRequestItem > linkedinUrlEqualsTriggering Record > LinkedIn URL
varRequestItem > clientReferenceIdEqualsTriggering Record > Lead ID
varRequestBody > contactsADDvarRequestItem
Where did the varRequestBody and varRequestItem variables come from? The varRequestBody variable was created during your configuration of the HTTP Callout step. It is an Apex-defined variable which contains the API request structure you'll be sending to Lusha. The varRequestItem variable will need to be created now: Click + New Resource → Resource Type: Variable → API Name: varRequestItem → Data Type: Apex-Defined → Apex Class: you must search for your external service classes containing "Lushav3" and select the one ending in _IN_body_contacts → Click Done.

5. Loop through the API Response from Lusha (For Each)

Add a Loop element BELOW the Action Step

  • Label: "For Each"
  • Collection Variable: {!Lusha.2XX.results}
  • Specify Direction: First item to last item

6. Capture any Error Code

Add an Assignment element BELOW the Loop Step

  • Label: "Error Code Capture Attempt"
  • Map Variable/Value Pair: varAttemptTitle EQUALS {!For_Each.jobTitle.title}
  • Map Variable/Value Pair: varAttemptErrorCode EQUALS {!For_Each.error.code}
Where did the varAttemptTitle and varAttemptErrorCode variables come from? Both of these variables will need to be created now, but thankfully they will be simple Text variables. For both, do the following: Click + New Resource → Resource Type: Variable → API Name: varAttemptTitle or varAttemptErrorCode → Data Type: Text → Default Value: leave this field blank → Click Done. If you would like to map any additional data coming from Lusha's API response, simply follow this same logic to create more text variables.

7. Loop through the Emails (Emails Loop Attempt)

Add a Loop element BELOW the Capture Step

  • Label: "Emails Loop Attempt"
  • Collection Variable: {!For_Each.emails}
  • Specify Direction: First item to last item

8. Capture any Email

Add an Assignment element BELOW the Loop Step

  • Label: "Emails Capture"
  • Map Variable/Value Pair: varAttemptEmail EQUALS {!Loop_Attempt_Emails.email} [create this text variable]

9. Loop through the Phones (Phones Loop Attempt)

Add a Loop element BELOW the Capture Step

  • Label: "Phones Loop Attempt"
  • Collection Variable: {!For_Each.phones}
  • Specify Direction: First item to last item

10. Capture any Phone

Add an Assignment element BELOW the Loop Step

  • Label: "Phones Capture"
  • Map Variable/Value Pair: varAttemptPhone EQUALS {!Loop_Attempt_Phones.z0number} [create this text variable]

At this point, you've created all the loops necessary to process any incoming Lusha data. Now you will close out the loops, update the triggering Lead, and log any error messages.

11. Map your Variables to the Lead (Map to Lead)

Add an Assignment element on the "After Last" path of the Phone Loop Attempt

  • Label: "Map to Lead"
  • Map Variable/Value Pair: {!$Record.Title} EQUALS {!varAttemptTitle}
  • Map Variable/Value Pair: {!$Record.Email} EQUALS {!varAttemptEmail}
  • Map Variable/Value Pair: {!$Record.Phone} EQUALS {!varAttemptPhone}

12. Confirm Any Matches (Match Found)

Add a Decision element on the "After Last" path of the For Each Loop element

  • Label: "Found a Match?"
  • Add a new Outcome:
    • Outcome Label: "Success?"
    • Condition Requirements: All Conditions Are Met (AND)
    • Map Resource/Value Pair: {!varAttemptErrorCode} Is Null {!$GlobalConstant.True}

13. Update the Lead (Update Records)

  • Label: "Update Records"
  • How to Find Records: Use the IDs and all field values from a record or record collection
  • Record or Record Collection: {!$Record}

At this point, you have completed configuring the full flow. All steps should be nested under the Asynchronous Path, and the concluding "Success?" path will lead to the Update Records step, while the "Default Outcome" should be left empty.

Testing and Activation

Debug

Save the Flow, click Debug, and tick Run Asynchronous Paths. Without this option the entire branch you have just built is skipped and it will appear that nothing happened.

Debug runs in rollback mode. Field values shown in the debug output will not persist to the record. A message reading "ready to be updated when the interview finishes" is expected and does not indicate a problem. Only a real trigger proves the write.

Activate and test

Activate the Flow, then create a Lead manually. The asynchronous path runs a few seconds after the insert commits, so fields will not populate instantly.

Appendix and Reference Information

Appendix A — Sample JSON Request

Paste this as the Sample JSON Request when configuring the HTTP Callout. Flow generates Apex properties only for keys present in this sample, so it must include every identifier you might ever send.

{
  "contacts": [
    {
      "clientReferenceId": "sfdc-lead",
      "firstName": "Orit",
      "lastName": "Shilvock",
      "companyName": "Lusha",
      "companyDomain": "lusha.com",
      "email": "orit.shilvock@lusha.com",
      "linkedinUrl": "https://www.linkedin.com/in/orit-shilvock-6243bb5"
    }
  ]
}
The reveal field is deliberately omitted. The API accepts an optional reveal array to control whether emails, phones, or both are unlocked. It is an array of primitive strings, which Flow's schema generator cannot type — including it prevents the callout from saving. Omitting it returns both emails and phones, which is the desired behaviour here.

Appendix B — Sample JSON Response

Paste this as the Sample JSON Response. Select Use Example Response rather than Connect.

{
  "requestId": "d2c14f20-3f35-430c-a71e-56ae51993af9",
  "results": [
    {
      "clientReferenceId": "sfdc-lead",
      "id": "v1.P47N0kkZlu-dq2E9I0HADOTv6XT5zlvT-g",
      "firstName": "Orit",
      "lastName": "Shilvock",
      "fullName": "Orit Shilvock",
      "jobTitle": {
        "title": "Vice President of Partnerships",
        "seniority": "Vice President",
        "startDate": "2025-01-20"
      },
      "company": {
        "id": "v1.A1gXjPYd63SSPjSvxO4uwqXqE5ilJV9S",
        "name": "Lusha",
        "domain": "www.lusha.com",
        "industry": "Technology, Information & Media"
      },
      "location": {
        "country": "Israel",
        "countryIso2": "IL",
        "state": "Tel Aviv District",
        "city": "Tel Aviv",
        "continent": "Asia",
        "isEuContact": false
      },
      "socialLinks": {
        "linkedin": "https://www.linkedin.com/in/orit-shilvock-6243bb5"
      },
      "partialProfile": false,
      "emails": [
        {
          "email": "orit.shilvock@lusha.com",
          "type": "work",
          "confidence": "A+",
          "updateDate": "2026-08-13"
        }
      ],
      "phones": [
        {
          "number": "+14155551234",
          "type": "mobile",
          "doNotCall": false,
          "countryIso2": "US",
          "updateDate": "2026-08-13"
        }
      ],
      "error": {
        "code": "NOT_FOUND",
        "message": "Contact not found"
      },
      "updateDate": "2026-08-13"
    }
  ],
  "billing": { "creditsCharged": 2, "resultsReturned": 1 }
}

Appendix C — Add an Entry Condition Formula

Set Condition Requirements to Formula Evaluates to True and paste the following. [See Appendix E for Suggested Custom Fields]

NOT({!$Record.Lusha_Enriched__c})
&& (
     NOT(ISBLANK({!$Record.Email}))
  || NOT(ISBLANK({!$Record.LinkedIn_URL__c}))
  || NOT(ISBLANK({!$Record.Website}))
  || (NOT(ISBLANK({!$Record.FirstName}))
      && NOT(ISBLANK({!$Record.LastName}))
      && NOT(ISBLANK({!$Record.Company})))
)

This makes the Flow identifier-agnostic. It fires whenever any combination of identifiers that Lusha accepts is present, and skips Leads that could not be matched anyway — which avoids spending credits on requests that cannot succeed.

Appendix D — Clean Up Input Domains/Websites

If your organization frequently captures inconsistent websites and domains, you can create a formula variable to automatically clean up URLs before adding them to your Lusha HTTP request.

Create the following Variable Formula:

formCompanyDomain

IF(ISBLANK({!$Record.Website}), "",
  LOWER(SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(
    {!$Record.Website}, "https://", ""), "http://", ""), "www.", ""))
)

This formula resource strips the protocol and www. prefix from the Lead's Website field so it can be sent as companyDomain.

Appendix E — Suggested Custom Fields

There are a number of custom fields you could create within Salesforce to measure this flow's effectiveness. All are optional, but see below for reference:

Field LabelAPI NameTypePurpose
Lusha EnrichedLusha_Enriched__cCheckbox (default unchecked)Prevents re-enrichment; audit flag
Lusha Contact IDLusha_Contact_Id__cText (255)Stable v3 entity ID for cheaper re-enrichment later
Lusha IndustryLusha_Industry__cText (255)Lusha industry taxonomy value
Lusha LinkedIn URLLusha_LinkedIn_URL__cURLReturned socialLinks.linkedin value
Lusha ErrorLusha_Error__cLong Text Area (32768)Stores the fault message if the callout fails
LinkedIn URLLinkedIn_URL__cURLOptional input identifier, if your org captures one on the Lead