TL;DR
This tutorial shows how to connect LinkedIn Lead Gen Forms to Salesforce using OpenClaw, then move the data into your CRM reliably with validation, dedupe, and retries. You will stand up an authenticated endpoint, verify the platform signature, transform fields into a clean Lead schema, and post records with safe error handling. The goal is a reproducible path from media click to a ready to work Lead for sales, with audit trails and a small blast radius. The primary keyword is workflow automation and you will see it implemented with concrete configs, not vague advice.
What You Will Build
You will create an inbound endpoint that receives LinkedIn Lead Gen payloads, validates authenticity, normalizes fields, deduplicates by person, and writes to Salesforce as a Lead. The playbook also captures consent metadata, retries transient failures, and parks irrecoverable items for review. If you prefer a hosted option, you can run the same design inside ButterGrow, which operates a managed OpenClaw instance.
Prerequisites
- A LinkedIn Ads account with Lead Gen Forms and developer app access.
- A Salesforce Sales Cloud org with API access and a connected app.
- An OpenClaw workspace. If you do not have one yet, you can get started in minutes and follow the onboarding flow.
- Secrets created for your LinkedIn app secret and Salesforce OAuth values.
For a tour of capabilities that matter for this integration, see the AI marketing automation features overview.
Architecture Overview
The flow has five stages.
- LinkedIn posts a form submission to your webhook with a signature header and a form payload.
- OpenClaw verifies the signature, rejects invalid requests, and parses the payload.
- A transform normalizes fields and attaches campaign metadata.
- A dedupe gate checks for a recent identical submission and short circuits duplicates.
- A Salesforce writer searches for an existing Lead by email, then creates or updates and logs the outcome.
For a similar pattern that writes to HubSpot, compare steps with our LinkedIn Leads to HubSpot integration guide.
Implementation Steps
Step 1Create your LinkedIn developer app and webhook
- In LinkedIn's developer portal, register an app and request access for Lead Gen Forms. Add a webhook URL that will point to your OpenClaw endpoint and save your app secret.
- Subscribe the app to Lead Gen events and complete the challenge phase by echoing back the verification token from the test request.
- Record the header names used for signature verification and timestamps so you can implement checks in your handler.
Keep your app secret in OpenClaw's secret store. Avoid hard coding it in source files or environment variables that leak to logs.
Step 2Scaffold an OpenClaw playbook with an HTTP trigger
Create a minimal playbook with one HTTP input, a transform, and a Salesforce writer. The example below uses namespaced steps so you can extend later without breaking contracts.
# file: playbooks/linkedin-to-salesforce.yml
version: 1
name: linkedin-to-salesforce
triggers:
- id: inbound
type: http
method: POST
path: /hooks/linkedin/lead
auth: none
rateLimit:
unit: minute
limit: 300
secrets:
- name: LINKEDIN_APP_SECRET
- name: SF_CLIENT_ID
- name: SF_CLIENT_SECRET
- name: SF_REFRESH_TOKEN
- name: SF_LOGIN_URL # https://login.salesforce.com or https://test.salesforce.com
steps:
- id: verify
uses: core.hmac-verify
with:
algo: sha256
secret: ${secrets.LINKEDIN_APP_SECRET}
header: x-li-signature
timestampHeader: x-li-signature-ts
maxSkewSeconds: 300
- id: transform
uses: core.transform
with:
script: transform/linkedin-lead.ts
- id: dedupe
uses: core.idempotency
with:
key: ${steps.transform.output.idempotencyKey}
ttlSeconds: 86400
- id: salesforce-auth
uses: salesforce.oauth-refresh
with:
clientId: ${secrets.SF_CLIENT_ID}
clientSecret: ${secrets.SF_CLIENT_SECRET}
refreshToken: ${secrets.SF_REFRESH_TOKEN}
loginUrl: ${secrets.SF_LOGIN_URL}
- id: upsert-lead
if: ${steps.dedupe.output.skipped == false}
uses: salesforce.rest
with:
method: POST
path: /services/data/v60.0/composite
headers:
Authorization: Bearer ${steps.salesforce-auth.output.access_token}
bodyFrom: scripts/sf-upsert-lead.json
retry:
policy: exponential
maxAttempts: 5
backoffSeconds: 2
deadLetter:
topic: linkedin-salesforce-dlq
This scaffolding keeps signature verification and OAuth separated from data mapping so each piece can be tested independently.
Step 3Verify the signature and freshness of incoming requests
LinkedIn includes a signature header that you can validate with HMAC SHA256. The snippet below shows a simple check in TypeScript that compares a computed digest to the value in x-li-signature and enforces a five minute window using an x-li-signature-ts header.
// file: transform/verify.ts
import crypto from "node:crypto";
export function verifySignature({ rawBody, headers, secret }: { rawBody: Buffer; headers: Record<string, string>; secret: string }) {
const provided = headers["x-li-signature"] || "";
const ts = Number(headers["x-li-signature-ts"] || 0);
const now = Math.floor(Date.now() / 1000);
if (!provided || !ts || Math.abs(now - ts) > 300) return { ok: false, reason: "stale or missing signature" };
const digest = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const ok = crypto.timingSafeEqual(Buffer.from(provided, "hex"), Buffer.from(digest, "hex"));
return ok ? { ok: true } : { ok: false, reason: "mismatch" };
}
If the check fails, return a 401 to stop processing early. Keep error messages generic and do not leak details about your verification logic.
Step 4Normalize and map fields to Salesforce
Start with a small, explicit mapping. Extend only when sales requests a new field or when you see high frequency unmapped attributes in logs.
| LinkedIn field | Salesforce field | Required |
|---|---|---|
| firstName | FirstName | yes |
| lastName | LastName | yes |
| yes | ||
| companyName | Company | no |
| jobTitle | Title | no |
| phoneNumber | Phone | no |
| consent.timestamp | Consent_Captured_At__c | no |
| consent.answer | Consent_Status__c | no |
Transform script example that also builds a stable idempotency key from the platform lead id and form id.
// file: transform/linkedin-lead.ts
type In = {
lead: { id: string; formId: string; firstName?: string; lastName?: string; email?: string; companyName?: string; jobTitle?: string; phoneNumber?: string };
consent?: { answer?: string; timestamp?: string };
campaign?: { id?: string; name?: string };
};
export default function run(input: In) {
const lead = input.lead || ({} as In["lead"]);
const idempotencyKey = `${lead.id}:${lead.formId}`;
const out = {
FirstName: lead.firstName || "",
LastName: lead.lastName || "",
Email: lead.email || "",
Company: input.lead.companyName || "",
Title: input.lead.jobTitle || "",
Phone: input.lead.phoneNumber || "",
Consent_Captured_At__c: input.consent?.timestamp || null,
Consent_Status__c: input.consent?.answer || null,
Campaign_Source__c: input.campaign?.name || null,
Campaign_Id__c: input.campaign?.id || null,
};
return { body: out, idempotencyKey };
}
Step 5Authenticate to Salesforce with OAuth 2.0
In Salesforce, create a connected app with the API scope and a permission set that allows read and create on the Lead object. Use the refresh token flow so automations continue to run without manual intervention. Store the client id, client secret, login URL, and refresh token as OpenClaw secrets, not as inline config.
The scaffolded salesforce.oauth-refresh step exchanges the refresh token for an access token. If the exchange fails, the retry policy will back off before moving the item to the DLQ.
Step 6Upsert the Lead using the Composite API
The Salesforce Composite API lets you perform a find then create or update sequence in a single call. The following template queries for an existing Lead by email and either updates it or creates a new record. Save it as scripts/sf-upsert-lead.json.
{
"allOrNone": false,
"compositeRequest": [
{
"method": "GET",
"url": "/services/data/v60.0/query?q=SELECT+Id,Email+FROM+Lead+WHERE+Email='@{steps.transform.output.body.Email}'",
"referenceId": "findLead"
},
{
"method": "PATCH",
"url": "/services/data/v60.0/sobjects/Lead/@{$.compositeResponse[0].body.records[0].Id}",
"body": "@{steps.transform.output.body}",
"referenceId": "updateLead",
"condition": "@{$.compositeResponse[0].body.totalSize} > 0"
},
{
"method": "POST",
"url": "/services/data/v60.0/sobjects/Lead",
"body": "@{steps.transform.output.body}",
"referenceId": "createLead",
"condition": "@{$.compositeResponse[0].body.totalSize} == 0"
}
]
}
This approach avoids custom external id fields. It also keeps your transform script simple by placing the branching logic inside the API template.
Step 7Add idempotency and duplicate protection
Even with search before write, network retries can produce duplicates. Keep a short lived idempotency record keyed by the composite of LinkedIn lead id and form id. Skip processing when a key is present.
// file: transform/idempotency.ts
import { kv } from "openclaw:kv";
export async function guard(key: string) {
const exists = await kv.get(key);
if (exists) return { skipped: true };
await kv.set(key, "1", { ttlSeconds: 86400 });
return { skipped: false };
}
For a deeper dive into durability strategies, see our Idempotency, retries, and DLQs guide.
Step 8Configure retries and a dead letter queue
Use exponential backoff for transient errors like HTTP 429 or 5xx. Send exhausted items to a DLQ topic so you can inspect payloads, error codes, and context without blocking the main path. Log correlation ids to trace a lead from webhook to CRM write.
Step 9Test the flow locally and in a sandbox
Emulate a LinkedIn payload with curl. Replace placeholders with your values and point to your OpenClaw URL. Keep the body close to the real schema so your transform script gets exercised.
curl -X POST "https://your-runner.example/hooks/linkedin/lead" \
-H "content-type: application/json" \
-H "x-li-signature: REPLACE_WITH_TEST_VALUE" \
-H "x-li-signature-ts: $(date +%s)" \
-d '{
"lead": {
"id": "1234567890",
"formId": "987654321",
"firstName": "Ava",
"lastName": "Lopez",
"email": "ava@example.com",
"companyName": "Acme Co",
"jobTitle": "Marketing Manager",
"phoneNumber": "+1 415 555 0101"
},
"consent": { "answer": "granted", "timestamp": "2026-10-01T12:30:00Z" },
"campaign": { "id": "C-001", "name": "Q4 Lead Gen" }
}'
After each test, confirm the Lead appears in Salesforce with the mapped fields and the correct campaign metadata. Use a sandbox org first, then switch the login URL to production once you are satisfied.
Step 10Promote safely and monitor
Add change controls and limit who can edit secrets and playbooks. Set up basic dashboards for request volume, success rate, median latency, and DLQ size, and alert on abnormal spikes. If you want a fast reference for controlled rollouts and approvals, our comparison guide for gated rollouts in OpenClaw shows how to stage changes using approval windows.
If you want this integration without maintaining keys, runners, and monitors, ButterGrow ships the same pattern on a hosted OpenClaw stack. You can use the get started in minutes path to deploy a production ready pipeline, see the AI marketing automation features, and review common setup questions before inviting your sales team.
References
- Salesforce REST API Developer Guide - Endpoints for sObjects, Composite API, and authentication details.
- Salesforce OAuth 2.0 Web Server Flow - How to create a connected app and use refresh tokens in server side automations.
Frequently Asked Questions
How do I verify the LinkedIn Lead Gen webhook signature header in OpenClaw?+
Use an HMAC SHA256 check against the raw request body with the app secret and compare it to the x-li-signature header from LinkedIn. Reject requests when the signature is missing, the timestamp is too old, or the computed value does not match. Keep the secret in OpenClaw's secret store rather than in plain config.
What is the safest way to authenticate to Salesforce from an automation playbook?+
Create a connected app in Salesforce and use OAuth 2.0 with a minimum permission set that can read and write the Lead object. Store the client id, client secret, and refresh token as secrets. Refresh access tokens automatically and rotate credentials on a regular schedule, starting in a sandbox before promoting to production.
How should I map LinkedIn form fields to Salesforce Lead fields?+
Decide on a stable mapping that covers first name, last name, email, company, job title, and consent timestamp. Keep a JSON map in version control and use a transform step to normalize values such as country codes and phone formats. Log unmapped fields so you can enrich your schema without dropping data.
How can I prevent duplicate Leads when the same person submits multiple LinkedIn forms?+
Use an idempotency key derived from the LinkedIn lead id plus the form id and keep a short TTL record in a key value store. In Salesforce, search for an existing Lead by email before creating a new one, then update the existing record with the latest form and campaign metadata. This keeps history without inflating lead counts.
What is the best way to test the integration end to end without spending ad budget?+
Trigger the OpenClaw endpoint with a sample payload that mirrors LinkedIn's schema using curl. In Salesforce, point the automation to a sandbox and create a test campaign for attribution. Validate that the Lead appears with the mapped fields, then exercise error paths like invalid tokens and retry behavior.
How do I handle consent and opt out values from LinkedIn in Salesforce?+
Capture LinkedIn's consent answers and timestamps and map them to explicit Salesforce fields, such as a custom Consent_Status picklist and a Consent_Captured_At datetime. Never assume consent is global. Use these values to drive email and messaging policies rather than a single boolean flag.
Ready to try ButterGrow?
See how ButterGrow can supercharge your growth with a quick demo.
Book a Demo