Engineering Logs PPC Technical Ownership LinkedIn Ads CAPI Integration
Server-Side Tracking LinkedIn CAPI WordPress WooCommerce

LinkedIn Ads CAPI Integration for WordPress.

LinkedIn's Conversions API is harder to set up than Meta's or Google's. OAuth tokens, strict URN formatting, and a professional identity graph that matches differently. This guide covers every layer — authentication, event mapping, deduplication, and the li_fat_id persistence problem that kills B2B match rates.

Brady Christensen · Updated 2025 · 20 min read

01What LinkedIn CAPI Actually Is

The LinkedIn Conversions API (CAPI) is a direct connection between your server and LinkedIn's ad platform. Instead of tracking conversions through a browser pixel, your server sends the data directly to LinkedIn after a purchase or lead event fires.

This matters because browser tracking is unreliable. Ad blockers, Apple's Intelligent Tracking Prevention (ITP), and iOS privacy changes all strip data from client-side pixels. When signals disappear, LinkedIn's algorithm can't optimize. ROAS looks wrong. Retargeting audiences shrink.

The CAPI moves conversion data off the browser entirely. LinkedIn gets the data regardless of what the visitor's device or browser does.

How LinkedIn Differs from Meta and Google

Meta's Conversions API matches on a consumer identity graph — personal emails, phone numbers, location. Google Enhanced Conversions matches on search intent and click IDs. LinkedIn matches on a professional identity graph: work emails, job titles, company names, and its own click ID called li_fat_id. The matching logic is fundamentally different, which is why LinkedIn CAPI requires different data preparation.

02CAPI vs Insight Tag: Why You Need Both

The LinkedIn Insight Tag is a browser-based pixel. It captures page views, session data, and early-funnel behavior. The CAPI captures confirmed conversions from your server. Running both together is the recommended hybrid model.

The Insight Tag handles top-of-funnel visibility. The CAPI guarantees transaction data arrives — even when the browser pixel gets blocked. When both fire for the same event, LinkedIn deduplicates them automatically.

// hybrid tracking architecture — linkedin capi + insight tag
Browser layer — fast, contextual, blockable
LinkedIn ad click
Lands on site
Insight Tag fires
PageView / Lead
LinkedIn
Server layer — unblockable, PII-rich, guaranteed
Order confirmed
WooCommerce backend
CAPI fires
Purchase event
LinkedIn
Deduplication: both payloads carry the same eventId → LinkedIn keeps 1 conversion, discards the server-side duplicate.

03OAuth Authentication and Token Setup

This is where LinkedIn CAPI gets harder than Meta or Google. LinkedIn requires OAuth 2.0 authentication for every API call. There is no simple static API key you can drop into a config file and move on.

You have two paths depending on your situation:

Path 1: Direct access token (most WordPress sites)

If you manage your own ad account, you can generate a non-expiring access token directly inside LinkedIn Campaign Manager. Go to Signals Manager → Conversions → Settings → Generate Access Token. This token works immediately and does not require building an OAuth app.

Token Permissions Matter

The token must carry two specific OAuth scopes: rw_conversions (read-write conversions) and r_ads (read ads). The user who generates the token must also have an explicit role on the ad account — Account Manager, Campaign Manager, or Billing Admin. A token with missing scopes returns a 403 error and will silently drop all your data.

Path 2: OAuth app (agencies and multi-account setups)

Agencies managing multiple client accounts need to build a proper OAuth application. This requires registering through the LinkedIn Developer Portal, applying for the Conversions API product, verifying your company page, and implementing a 3-legged OAuth flow. This process takes longer and requires LinkedIn approval.

The required HTTP headers

Every API call must include these headers exactly. Missing any one of them causes a hard rejection:

required-headers.json HTTP
Authorization: Bearer YOUR_ACCESS_TOKEN Content-Type: application/json LinkedIn-Version: 202405 X-Restli-Protocol-Version: 2.0.0

04Payload Structure and WooCommerce Event Mapping

LinkedIn uses a fixed set of conversion event types. You must map WooCommerce actions to these names exactly. The wrong name means data is accepted but never attributed to a campaign.

WooCommerce ActionLinkedIn EventNotes
Form submission / contactLeadPrimary B2B event. Maps to any lead gen form on WordPress.
Account registrationSign UpNew user account created in WooCommerce.
Item added to cartAdd To CartRecommended for ecommerce; helps train the algorithm.
Payment confirmedPurchaseFire from woocommerce_payment_complete hook — not the thank you page.

Below is the full Purchase payload structure for the LinkedIn CAPI endpoint at /rest/conversionEvents:

purchase-payload.json JSON
{ "conversion": "urn:lla:llaPartnerConversion:CONVERSION_RULE_ID", "conversionHappenedAt": 1700000000000, // UNIX ms — NOT seconds "eventId": "wc_order_5291", // Must match Insight Tag exactly "conversionValue": { "currencyCode": "USD", // ISO 4217 "amount": "149.00" // String, not float }, "user": { "userIds": [{ "idType": "SHA256_EMAIL", "idValue": "SHA256_HASH_OF_LOWERCASED_EMAIL" }, { "idType": "LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID", "idValue": "VALUE_OF_li_fat_id" }], "userInfo": { "firstName": "John", // Plain text, no hashing "lastName": "Smith", "companyName": "Acme Corp", "title": "Marketing Director", "countryCode": "US" } } }
Timestamp Format Error — Most Common Mistake

LinkedIn requires conversionHappenedAt in milliseconds, not seconds. PHP's time() returns seconds. You must multiply by 1000. Sending seconds causes a 400 INVALID_CONVERSION_TIME_FIELD_VALUE error that is easy to miss in logs.

05Match Keys and the li_fat_id Problem

LinkedIn matches server events to user profiles using identifying data you include in the payload. The more identifiers you send, the higher your match rate. But not all identifiers are equal — and one in particular is critical for B2B attribution.

SHA256_EMAIL
userIds[].idType
High Priority
Lowercase and trim the email before hashing. Using a personal email that doesn't match the LinkedIn profile causes a silent mismatch. B2B purchases often use company emails — these match better.
LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID
userIds[].idType
Strongest Match
The li_fat_id value from the URL parameter or first-party cookie. This is deterministic — if present, attribution is near-certain. See the persistence problem below.
First + Last Name
userInfo.firstName / lastName
Supporting
Both must be sent together. Plain text — no hashing. Adds signal when email alone doesn't resolve to a profile.
Company + Title
userInfo.companyName / title
B2B Booster
Critical for B2B setups. LinkedIn's graph is built on professional identity — company and title data significantly improves match confidence.

The li_fat_id persistence problem

When someone clicks a LinkedIn ad, LinkedIn appends li_fat_id to the destination URL. This is the strongest match key you have. The problem: browser-based cookies get deleted by ITP within 7 days. B2B sales cycles last weeks or months.

If you rely on a browser cookie to preserve li_fat_id, it will be gone long before the WooCommerce purchase fires. Your server payload arrives with no deterministic identifier — only hashed email — and match rates collapse.

Fix: Server-Side li_fat_id Storage

When a visitor lands from a LinkedIn ad, capture li_fat_id from the URL immediately. Store it in a server-side, HTTP-only cookie or in the WooCommerce session/user meta. When the purchase event fires — weeks later, if needed — retrieve it from storage and include it in the API payload. This is what separates a 4/10 match score from an 8+/10 match score.

Running the same li_fat_id problem with Meta? The same persistence architecture applies to Facebook's Conversions API. Our fix guide covers both platforms side by side.

Meta CAPI Dedup Fix →

06Implementation Paths for WordPress

Four main approaches exist. Your choice depends on traffic volume, team capacity, and how much control you need over the payload.

Option A: Plugin-based (lowest effort, limited control)

Plugins like PixelYourSite and Pixel Manager for WooCommerce support LinkedIn CAPI to varying degrees. They handle OAuth token storage and basic event mapping automatically. The tradeoff: you can't control the exact payload structure, and they may not support li_fat_id persistence correctly. Plugin updates also lag behind LinkedIn's API deprecation cycles.

Option B: GTM Server-Side (recommended for most stores)

Deploy a Google Tag Manager Server container on a custom subdomain like metrics.yourstore.com. The web container captures WooCommerce dataLayer events. The server container formats the payload and fires the LinkedIn CAPI call. This approach keeps tracking logic off your WordPress server and gives you full payload control through a visual interface.

Stape.io vs Google Cloud for Your GTM Server

Stape.io has a fixed monthly cost and a visual payload log — you can inspect historical events without writing SQL. Google Cloud Run is cheaper at scale but requires query tools to debug. For most WordPress stores, Stape is faster to set up. Full comparison: Stape vs Google Cloud for GTM →

1

Deploy a GTM Server container on a custom subdomain

Map it to metrics.yourstore.com. Requests from the browser are treated as same-origin — ITP cannot block them. This is also how the li_fat_id cookie survives longer than 7 days.

2

Install the LinkedIn Conversions API tag template

Find it in the GTM Community Template Gallery. Add your access token and the Conversion Rule URN from LinkedIn Campaign Manager → Signals Manager.

3

Map WooCommerce dataLayer → LinkedIn parameters

WooCommerce and LinkedIn use different field names. Use GTM variables to transform them: order_ideventId, totalconversionValue.amount, billing_email → hashed SHA256_EMAIL.

4

Hash email in the browser container, not the server

SHA-256 hashing must happen in the web container — before data leaves the browser. Raw PII should never appear in server logs or transit between containers.

5

Pass li_fat_id from session storage to server payload

Capture li_fat_id on first page load and store it server-side. Include it in the GTM dataLayer push on the thank you page so the server container can pick it up.

Option C: Direct PHP via WooCommerce hooks

For teams that want full control without middleware, hook into woocommerce_payment_complete directly. This hook fires when an order transitions from pending to processing — server-side, with no dependency on the browser.

functions.php — linkedin-capi-hook PHP
add_action( 'woocommerce_payment_complete', function( $order_id ) { $order = wc_get_order( $order_id ); $email = strtolower( trim( $order->get_billing_email() ) ); $hashed_email = hash( 'sha256', $email );// Retrieve persisted li_fat_id from user/session meta $li_fat_id = get_post_meta( $order_id, '_li_fat_id', true );$payload = [ 'conversion' => 'urn:lla:llaPartnerConversion:YOUR_RULE_ID', 'conversionHappenedAt' => time() * 1000, // ms! 'eventId' => 'wc_order_' . $order_id, 'conversionValue' => [ 'currencyCode' => $order->get_currency(), 'amount' => (string) $order->get_total() ], 'user' => [ 'userIds' => [ [ 'idType' => 'SHA256_EMAIL', 'idValue' => $hashed_email ], [ 'idType' => 'LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID', 'idValue' => $li_fat_id ] ] ] ];wp_remote_post( 'https://api.linkedin.com/rest/conversionEvents', [ 'headers' => [ 'Authorization' => 'Bearer ' . YOUR_TOKEN, 'Content-Type' => 'application/json', 'LinkedIn-Version' => '202405', 'X-Restli-Protocol-Version' => '2.0.0', ], 'body' => json_encode( $payload ), ]); } );
Don't Use woocommerce_thankyou

The woocommerce_thankyou hook fires on the browser-rendered thank you page. Page caches, off-site gateways, and mobile users closing early can all prevent it from executing. Use woocommerce_payment_complete instead — it fires server-side, guaranteed, once payment clears.

Need this built correctly? Our tracking integrity service covers full CAPI implementation — authentication, payload mapping, li_fat_id persistence, and match quality validation.

View Tracking Services →

07How Deduplication Works on LinkedIn

When both the Insight Tag and the CAPI fire for the same event, LinkedIn uses the eventId field to merge them. Both payloads must carry the exact same string. One character difference means two conversions get recorded instead of one.

LinkedIn's dedup logic has one important quirk: when it finds a duplicate, it keeps the browser-side Insight Tag event and discards the server-side CAPI event. This is the opposite of Meta's behavior (Meta prefers the server signal). A working LinkedIn setup will show more conversions attributed to the Insight Tag than the CAPI in your dashboard — that is normal and correct.

✓  Success — 1 conversion recorded
Browser: wc_order_5291
+
Server: wc_order_5291
MERGED ✓

✗  Failure — 2 conversions recorded
Browser: random_uuid_x9k2
+
Server: wc_order_5291
DUPLICATE ✗
Browser generated a random ID. Server used the WooCommerce order ID. Different sources — dedup fails and ROAS looks inflated.

Attribution windows and lookback periods

Attribution TypeDefault WindowMax Configurable
Post-click30 days90 days (standard) / 365 days (Purchase)
View-through7 days30 days
Deduplication window48 hoursFixed

For B2B lead gen, the extended 365-day Purchase window is critical. A prospect who clicked your LinkedIn ad in Q1 and signed a contract in Q4 can still be attributed — if you've preserved the li_fat_id and sent the offline conversion payload within the window.

08Common Errors and What They Mean

LinkedIn CAPI errors fall into two types. Hard rejections come back as HTTP error codes — the payload never entered the system. Silent failures return 201 Created but the event never attributes to a campaign.

HTTP CodeErrorCause and Fix
400INVALID_CONVERSION_TIME_FIELD_VALUETimestamp is in seconds, not milliseconds. Multiply by 1000.
400INVALID_USER_IDENTIFICATION_FIELD_VALUENo valid identifier in the payload. You need at least one of: SHA256_EMAIL, li_fat_id, or firstName+lastName. Empty strings cause this.
401EMPTY_ACCESS_TOKENAuthorization header missing or malformed. Check the Bearer token format.
403USER_NOT_AUTHORIZEDToken missing rw_conversions scope, or the user doesn't have Campaign Manager role on the ad account.
422Unprocessable EntityThe conversion rule URN is wrong, deleted, or the attribution window type is unsupported for that event. Verify the URN in Campaign Manager.
201Unmatched (silent failure)Payload accepted but identity didn't resolve. Check email normalization and li_fat_id capture. Look for "Unmatched" in Campaign Manager.
500/504Server Error / TimeoutLinkedIn-side disruption. Implement exponential backoff retry logic in your PHP or GTM setup.

Matched vs Unmatched conversions

Matched means LinkedIn tied the event to a real member profile and searched for ad interactions. Unmatched means the payload arrived correctly but identity resolution failed. The data is useless for attribution or algorithm optimization.

A successful 201 response does not mean a successful attribution. Always check the matched/unmatched breakdown in Campaign Manager, not just your server logs.

Debugging tools

  • LinkedIn Payload Builder: Found in the Developer Portal. Build and test JSON structures before touching production code. It parses errors in real time and generates sample cURL commands.
  • Campaign Manager → Signals Manager: Shows active/inactive status for each conversion rule and event volume over time. A sudden drop here usually means a token expired or a hook stopped firing.
  • GTM Server Preview: Inspect exactly what enters the server container from WooCommerce and what leaves toward LinkedIn before going live.

09Edge Cases: B2B Leads, Offline Conversions, GDPR

B2B lead gen vs WooCommerce purchases

Most LinkedIn CAPI documentation assumes a single-event, single-session conversion. B2B doesn't work that way. A prospect clicks a LinkedIn ad, downloads a whitepaper, enters your CRM, and closes a deal four months later. The CAPI must handle both scenarios from the same WordPress installation.

For lead gen forms, fire the Lead event from the WooCommerce/WordPress backend when the form submits. Capture li_fat_id at the same time and store it with the contact record in your CRM. For the eventual purchase, retrieve the stored li_fat_id and include it in the CAPI payload — even if that payload fires months later.

Offline conversions

When deals close offline — phone calls, in-person meetings, CRM stage changes — there is no browser session to track. The CAPI supports asynchronous uploads with up to a 365-day lookback for Purchase events. When a deal closes in your CRM, trigger a server-side API call with the historical li_fat_id or email from the original lead record. LinkedIn attributes the offline revenue back to the ad campaign that started the relationship.

GDPR compliance

Server-side tracking does not bypass consent requirements. Sending hashed email or li_fat_id still constitutes processing personal data under GDPR. Your Consent Management Platform must gate both the Insight Tag and the CAPI independently.

Consent Must Gate the Server Hook

If a user declines advertising cookies, the woocommerce_payment_complete hook must be suppressed or fire without any PII — no email, no li_fat_id, no name. The consent state captured at checkout must be passed server-side and checked before the API call executes. This is your legal obligation, not LinkedIn's.

Order bumps and upsells

If you send a second Purchase event with the same eventId as the original order, LinkedIn treats it as a duplicate and discards it. You lose the upsell revenue data. Use a unique suffix for each additional transaction: wc_order_5291_bump_1.

10Validation and Match Quality Benchmarks

LinkedIn scores your integration with an Event Match Quality (EMQ) score from 1 to 10. This measures how well your identifiers resolve to real LinkedIn member profiles.

1–4
Low
Broken integration or missing identifiers. li_fat_id not captured. Email hashing failing. Fix immediately — data is not attributing.
5–7
Medium
Hashed email only, no li_fat_id. Probabilistic matching only. Professional email may not match the LinkedIn profile email.
8–10
High
Hashed email + li_fat_id + professional data all present. Deterministic matching on most events. Target for all Purchase events.

Data timing and the 72-hour rule

LinkedIn doesn't process CAPI events instantly. Allow up to 24 hours for identity resolution, plus another 48 hours for attribution modeling to update Campaign Manager reports. Always use a 72-hour trailing window when comparing WooCommerce order counts to LinkedIn conversion totals. Real-time reconciliation will always show a gap — this is expected.

What a healthy setup looks like in Campaign Manager

  • Conversion rule status shows Active with recent signal timestamps
  • Connection method shows "Web + CAPI" — both channels confirmed
  • Matched conversion percentage is above 70% for Purchase events
  • EMQ score is 8 or above for your highest-value conversion actions
  • Event volume within ±10% of WooCommerce gross order count on a 72-hour delay
Final Validation Checklist
Campaign Manager shows "Web + CAPI" connection for Purchase events
Conversion rule status is Active with recent timestamps
EMQ score is 8.0 or above for Purchase and Lead events
li_fat_id captured and stored server-side on first LinkedIn ad click
Timestamp sent in milliseconds, not seconds
OAuth token has rw_conversions scope and correct ad account role
Email lowercased and trimmed before SHA-256 hashing
eventId matches exactly between Insight Tag and CAPI payload
CAPI hook fires from woocommerce_payment_complete, not thank you page
Consent mode suppresses PII for users who declined tracking
Matched conversion rate above 70% in Campaign Manager
Reconciliation uses 72-hour trailing window, not real-time

Need this implemented and validated? We build the full LinkedIn CAPI stack — OAuth setup, GTM server container, li_fat_id persistence, PII hashing, and EMQ validation — as part of our PPC technical ownership service.

Get in Touch →