Lead Sync API Migration

LinkedIn Marketing Solutions is releasing a new Lead Sync API product. This migration guide facilitates the onboarding of existing developers to the new product and endpoints. If you fall into one or more of these categories, this migration guide is for you:

  • Currently have access to Ads Lead Sync and or Events Lead Generation and syncs leads
  • Currently have access to Advertising API and creates forms

NOTE: There are no schema changes for the Events Lead Generation API

Permissions for Lead Sync API

With the launch of the Lead Sync API, we’re introducing the following new permissions and endpoints to simplify syncing leads across different lead types:

Old API endpoint Old Permission Name Supported Lead Types Last Supported Version Last Supported Version will stop working on
/adFormQuestions r_ads_leadgen_automation SPONSORED 202307 December 16, 2024
/adForms r_ads_leadgen_automation SPONSORED 202307 December 16, 2024
/adFormResponses r_ads_leadgen_automation SPONSORED 202307 December 16, 2024
/adForms/Consents r_ads_leadgen_automation SPONSORED 202307 December 16, 2024
/leadNotificationUrls r_ads_leadgen_automation SPONSORED 202307 December 16, 2024
/leadGenFormQuestions r_events_leadgen_automation EVENT 202307 December 16, 2024
/leadGenForms r_events_leadgen_automation EVENT 202307 December 16, 2024
/eventFormResponses r_events_leadgen_automation EVENT 202307 December 16, 2024
/leadGenFormLegalInfo r_events_leadgen_automation EVENT 202307 December 16, 2024
/leadGenForms r_events_leadgen_automation EVENT 202307 December 16, 2024
/eventLeadNotifications r_events_leadgen_automation EVENT 202307 December 16, 2024
/leadGenFormVersions r_events_leadgen_automation EVENT 202307 December 16, 2024
/events r_events_leadgen_automation EVENT 202306 December 16, 2024
New API New Permission Name Supported Version starts from
/leadForms r_marketing_leadgen_automation
rw_ads
r_ads
202307
/leadFormResponses r_marketing_leadgen_automation 202307
/leadNotifications r_marketing_leadgen_automation 202307
/events r_events 202307

AdFormResponses APILeadFormResponses API

Workflow Changes

  • The new Lead Sync API supports fetching different types of leads based on the request (SPONSORED, EVENTS etc.)
  • The new associatedEntity field represents which entity the lead is collected from e.g. for the Sponsored use case, associatedEntity is the corresponding SponsoredCreativeUrn ad for the Event use case, associatedEntity is the corresponding EventUrn.
  • The /leadFormResponses API supports two different types of Owners. For the Sponsored use case, the owner is SponsoredAccountUrn. For Organic use cases like Event lead sync, the owner is OrganizationUrn.

Schema Changes

Schema

AdFormResponses API LeadGenFormResponses API
id id
adFormUrn versionedLeadGenFormUrn
sponsoredAccountUrn LeadGenFormOwner: SponsoredAccountUrn for Sponsored usecase, OrganizationUrn for other usecases.
SponsoredCampaignUrn SponsoredLeadMetadata: SponsoredCampaignUrn for Sponsored use case. Currently we don’t have other metadata information collected for non-sponsored usecase.
SponsoredCreativeUrn associatedEntity Union:
- SponsoredCreativeUrn for Sponsored usecase
- OrganizationUrn for Company Page usecase
- EventUrn for Event usecase
- StandardizedProductUrn for Product Page usecase
- OrganizationLandingPageUrn for Landing Page usecase.
leadType leadType
testLead testLead
submittedAt submittedAt

AdForms APILeadForms API

Workflow Changes

  • The new API supports both sponsored (previously called adForms) and organic LeadGen Forms with a single integration.
  • SponsoredAccount field is removed in favor of an Owner record which is a union of SponsoredAccountUrn and an OrganizationUrn.
  • Sponsored forms will have a sponsored account as an owner whereas organic forms will have organization as the owner.
  • Form fields now support multi locale strings. This means instead of providing just the string values marketers will need to provide locale and string value.
    • e.g. question field of a form being created in English locale
      "question": {
          "localized": {
             "en_US": "Last Name"
           }
        }
      
      Here Last Name is the question string value to be used for en_US locale.
    • e.g. question field of a form being created in Spanish locale
      "question": {
         "localized": {
            "es_ES": "Apellido"
          }
        }
      
      Here Apellido is the question string value to be used for es_ES locale.
  • Finder API supports fetching forms under sponsored account or an organization (organic forms).

Schema Changes

Schema

AdForms API LeadForms API Notes
id id & versionId id is still the unique identifier of the form. Version Id is for managing form edits.
locale creationLocale
reviewInfo reviewInfo
changeAuditStamps Flattened and simplified to created and lastModified fields
status state SUBMITTED maps to PUBLISHED
DRAFT maps to DRAFT
ARCHIVED maps to ARCHIVED
form.hiddenFields hiddenFields
account owner. Union of SponsoredAccountUrn for Sponsored usecase, OrganizationUrn for other usecases.
form.name name
form field Renamed to content

- headline & description are now multilocale strings

Within form.questions :
- question is now a multilocale string
- typeSpecificQuestionDetails renamed to questionDetails
- questionSubmissionCriteria is a new field to setup question submission criteria
- label is a new field serves as a unique identifier within the form to specify the question
form.consents, form.privacyPolicy, form.legalDisclaimer Moved under content.legalInfo

- legalDisclaimer is now a multilocale string
- privacyPolicy renamed to privacyPolicyUrl

Within form.consents :
- consentId renamed to id
- consentRequired renamed to checkRequired
- consent is now a multilocale string
form.landingPage, form.thankYouMessage, form.thankYouPageCallToAction Moved under content.postSubmissionInfo
-thankYouMessage is now a multilocale string

LeadNotificationUrlsLeadNotifications API

Workflow Changes

  • LeadNotifications API supports storing notification URLs (webhooks) disassociated with ad entities (i.e. old API requires an account/campaign/creative to be tied to the notification object).
  • Support storing notification URLs at varying levels of granularity, most notably at the form level.
  • Support storing URLs for the same form/owner under different lead types, such as SPONSORED and EVENT.
  • Support push notifications for lead submissions as well as deletions.

Schema Changes

Schema

LeadNotificationUrls API LeadNotifications API Notes
key Instead of a key, please use the following fields to specify the level of granularity that notifications should be sent:
- owner
- leadType
- versionedFormUrn
- associatedEntity
For example, to subscribe to notifcations at the sponsored account level, set the owner field to your specific SponsoredAccountUrn and set leadType to SPONSORED.
status Removed. All subscriptions are active by default. To stop receiving a notifications under a subscription, please delete it.
URL webhook

Notification Payload

LeadNotificationUrls API LeadNotifications API Notes
createdTime occuredAt
accountUrn owner For the SPONSORED lead type the owner is a sponsored account urn. For all other lead types the owner is an organization urn.
campaignUrn Removed. Only sponsored leads will have a campaignUrn, in this case the campaign and related sponsored metadata can be obtained by fetching the lead directly.
creativeUrn associatedEntity For the SPONSORED lead type the creative urn will be returned via the associatedEntity field. For other lead types this field may resolve to other urns.
formUrn leadGenForm This will be a VersionedLeadGenFormUrn instead of an AdFormUrn.
adFormResponseUrn leadGenFormResponse This will be a LeadGenFormResponseUrn instead of an AdFormResponseUrn
N/A type Will always be set to "LEAD_ACTION"
N/A leadType Type of the lead, will be set to "SPONSORED" for sponsored leads
N/A leadAction Action performed on the lead, will be either "CREATED" or "DELETED"

For example, consider the following JSON in the old payload format:

{ 
  "createdTime": 1489428028784, 
  "accountUrn": "urn:li:sponsoredAccount:67890", 
  "campaignUrn": "urn:li:sponsoredCampaign:789012", 
  "creativeUrn": "urn:li:sponsoredCreative:11223344", 
  "formUrn": "urn:li:adForm:123456", 
  "adFormResponseUrn": "urn:li:adFormResponse:abc123-abab-abab-abab-abc123abc123" 
}

The equivalent JSON in the new payload format would look like:

{
    type: “LEAD_ACTION”,
    leadGenFormResponse: "urn:li:leadGenFormResponse:abc123-abab-abab-abab-abc123abc123",
    leadGenForm: "urn:li:versionedLeadGenForm:(urn:li:leadGenForm:123456, 1)",
    owner: {"sponsoredAccount": "urn:li:sponsoredAccount:67890"},
    associatedEntity:  {"sponsoredCreative": "urn:li:sponsoredCreative:11223344"},
    leadType: "SPONSORED"
    leadAction: "CREATED"
    occurredAt: 1489428028784
}

Duplicate Webhooks

Please note that legacy webhooks (LeadNotificationUrls) will continue to receive notifications in the old payload format until the legacy APIs reach end of life. This means that you will receive duplicate notifications if you have also subscribed using the same parameters via the new LeadNotifications API. Proper deduplication logic should be put in place.

Setting up a webhook via LeadNotificationUrls API vs LeadNotifications API

  • LeadNotificationUrls API create request for a sponsored account webhook
curl -X POST 'https://api.linkedin.com/rest/leadNotificationUrls' \
--H 'X-Restli-Protocol-Version: 2.0.0' \
--H 'Authorization: Bearer {INSERT_TOKEN}' \
--H 'LinkedIn-Version: {version number in the format YYYYMM}' \
--H 'Content-Type: application/json' \
--data '{
    "key": {
        "developerApplication": "urn:li:developerApplication:12345",
        "sponsoredEntity": "urn:li:sponsoredAccount:67890"
    },
    "status": "ACTIVE",
    "url": "[https://www.example.com](https://eogi5ry4pv6izmq.m.pipedream.net)"
}'
  • LeadNotifications API create request for a sponsored account webhook
curl -X POST 'https://api.linkedin.com/rest/leadNotifications”' \
--H 'X-Restli-Protocol-Version: 2.0.0' \
--H 'Authorization: Bearer {INSERT_TOKEN}' \
--H 'LinkedIn-Version: {version number in the format YYYYMM}' \
--H 'Content-Type: application/json' \
--data '{
    "webhook": "https://eogi5ry4pv6izmq.m.pipedream.net",
    "owner": {
        "sponsoredAccount": "urn:li:sponsoredAccount:67890"
    },
    "leadType": "SPONSORED"
}'

FAQ

Q: If my developer app already had access to the legacy APIs and permissions, do I need to request access to the new Lead Sync API?

No. Your existing developer app should already have access to the new endpoints and permissions.

Q: What if I’ve migrated to the new lead sync APIs but I have not migrated any existing webhook subscriptions yet? This means I’m still receiving the legacy webhook notification payload but using the new Lead Sync APIs to get back the form response data. Can I use the new GET /leadFormResponses/{id} endpoint using the id in the legacy webhook notification payload? E.g. urn:li:adFormResponse:abc123-abab-abab-abab-abc123abc123

Yes, however please note that the new /leadFormResponses/{id} endpoint takes in an ID, abc123-abab-abab-abab-abc123abc123, and not an URN.

Response Decoration Migration

Below are examples of the top API response decoration usage and how to achieve the same via the new Lead Sync APIs. Some fields are not returned by default in the API request response therefore, to get all fields you may require, you will need to use field projection. Field projection allows you to choose exactly what fields you'd like returned in the request response.

If the additional metadata you seek is not returned, making a separate API request may be required. For example, to get the campaign group, make an API request to the GET campaign by ID endpoint and view the campaignGroupInfo field.

Response decoration migration for adFormResponses API

adFormResponses leadFormResponses field projection example
form~(form(hiddenFields, landingPage, name, id)) form.hiddenFields --> form.hiddenFields[]
form.landingPage -->
form.content.postSubmissionInfo.callToAction.callToActionTarget.landingPageUrl
form.name --> form.name
form.id --> form.id
fields=form:(hiddenFields,name,id,content:(postSubmissionInfo:(callToAction:(callToActionTarget:(landingPageUrl)))))
question~(name, question, typeSpecificQuestionDetails, predefinedField, questionId) question.name --> form.content.questions[].name
question.question --> form.content.questions[].question
question.typeSpecificQuestionDetails --> form.content.questions[].questionDetails
question.predefinedfield --> form.content.questions[].predefinedField
question.questionId --> form.content.questions[].questionId
fields=form:(content:(questions))
consent~(content, consentId, consentRequired) consent.content --> form.content.legalInfo.consents[].consent
consent.consentId --> form.content.legalInfo.consents[].Id
consent.consentRequired --> form.content.legalInfo.consents[].checkRequired
fields=form:(content:(legalInfo:(consents)))
account~(id, name) account.id --> owner.sponsoredAccount (parse out just the numbers)
account.name --> ownerInfo.sponsoredAccountInfo.name
fields=owner,ownerInfo:(sponsoredAccountInfo)
campaign~(id, name, type) campaign.id --> leadMetadataInfo.sponsoredLeadMetadataInfo.campaign.id (parse out just the numbers)
campaign.name --> leadMetadataInfo.sponsoredLeadMetadataInfo.campaign.name
campaign.type --> leadMetadataInfo.sponsoredLeadMetadataInfo.campaign.type
fields=leadMetadataInfo:(sponsoredLeadMetadataInfo:(campaign:(id,name,type)))
creative~(id, intendedStatus, content) creative.id --> associatedEntityInfo.associatedCreative.id (parse out just the numbers)
creative.intendedStatus --> associatedEntityInfo.associatedCreative.intendedStatus
creative.content --> associatedEntityInfo.associatedCreative.content.reference (make additional API call to the /posts endpoint to get additional metadata if required)
fields=associatedEntityInfo:(associatedCreative:(id,intendedStatus,content:(reference)))

Response decoration migration for eventFormResponses API

eventFormResponses leadFormResponses field projection example
form~(hiddenFields, landingPage, name, id) form~.hiddenFields --> form.hiddenFields[]
form~.landingPage --> form.content.postSubmissionInfo.callToAction.landingPageUrl
form~.name --> form.name
form~.id --> form.id
fields=form:(hiddenFields,name,id,content:(postSubmissionInfo:(callToAction:(callToActionTarget:(landingPageUrl)))))
event~(name,id) event~.name --> make an additional api call to the /events endpoint using the id
event~.id --> id
/leadFormResponses?fields=id
question~(name) question~.name --> form.content.questions[].name fields=form:(content:(questions))

The examples in the table above are very specific to show how granular you can get with field projection however, for the data we want, getting that granular isn't necessary. The following example simplifies the request as either way, the json request response will need to be parsed. fields=ownerInfo,associatedEntityInfo,leadMetadataInfo,owner,leadType,versionedLeadGenFormUrn,id,submittedAt,testLead,formResponse,form:(hiddenFields,creationLocale,name,id,content).