# Migrate from LinkedIn Ads

Every LinkedIn Marketing API ads call you make today, the Zernio endpoint that replaces it and the fields to send, plus the calls with no Zernio equivalent.

Each table maps a LinkedIn Marketing API call to the Zernio endpoint that does the same job, with the fields to send.

Read the first two tables before migrating any call. Every row below assumes them.

## Rules for every call

| Topic | LinkedIn Marketing API | Zernio |
|---|---|---|
| Base URL | `https://api.linkedin.com/rest/...` | `https://zernio.com/api/v1/...` |
| Auth | OAuth token with `rw_ads`, `r_ads`, `r_ads_reporting`, `rw_conversions`, `r_marketing_leadgen_automation`, `rw_dmp_segments` | `Authorization: Bearer YOUR_ZERNIO_API_KEY` only. Zernio holds the LinkedIn token and its scopes |
| Headers | `Linkedin-Version: YYYYMM`, `X-Restli-Protocol-Version: 2.0.0` | None. Zernio sends them and moves the version |
| Updates | `POST` with `X-RestLi-Method: PARTIAL_UPDATE` and `{"patch": {"$set": {...}}}` | `PUT` or `PATCH` with the fields to change |
| Batches | `BATCH_CREATE`, `BATCH_PARTIAL_UPDATE` with `ids=List(...)` | One call per object, except `POST /v1/ads/campaigns/bulk-status` and conversion events |
| URNs | Encoded URNs in queries and bodies (`urn%3Ali%3AsponsoredCampaign%3A123`) | Plain numeric ids in paths and bodies (`123`). See the ids table |
| Money | `{"amount": "50.00", "currencyCode": "USD"}` | Create: `budgetAmount: 50` and `budgetType` (`daily` or `lifetime`). Updates: `budget: {amount: 50, type: "daily"}`. Whole units of the ad account's currency |
| Status on status endpoints | `ACTIVE`, `PAUSED`; creatives use `intendedStatus` | `active`, `paused` (lowercase) on every `/status` endpoint |
| Status on create bodies | `ACTIVE`, `PAUSED`, `DRAFT` | `ACTIVE`, `PAUSED` on `POST /v1/ads/create` |
| Delete | `DELETE` on drafts, `PENDING_DELETION` otherwise | `DELETE` on the resource; Zernio picks the right one |
| Several objects in one request | Group, campaign, post and creative as separate calls | `POST /v1/ads/create` builds them all in one call |
| Pagination | `pageSize` and `pageToken`, or `start` and `count` | `page` and `limit` on synced lists; `cursor` or `after` where an endpoint says so |
| Reads | Always live | `/v1/ads`, `/v1/ads/campaigns`, `/v1/ads/ad-sets`, `/v1/ads/tree` and `/v1/ads/performance` read Zernio's synced copy. Accounts, audiences, leads, forms, targeting, conversions, tags and analytics breakdowns are live |
| Rate limits | Per app and per member, shown in the Developer Portal | LinkedIn's own limits still apply; Zernio retries a `429` three times before returning it |

## Ids

| What | LinkedIn Marketing API | Zernio |
|---|---|---|
| Connection | Your OAuth token | `accountId`: connect with `GET /v1/connect/{platform}/ads`, `platform` set to `linkedin` and `profileId`. The connection has platform `linkedinads` in `GET /v1/accounts` |
| Posting connection | A member or Page token | The `linkedin` account that posts to the Company Page. `POST /v1/ads/boost` takes this one, not the `linkedinads` one |
| Ad account | `urn:li:sponsoredAccount:507404993` | `adAccountId` = `507404993`. List with `GET /v1/ads/accounts?accountId=...` (`accounts[].id`) |
| Campaign group | `urn:li:sponsoredCampaignGroup:111` | `campaignId` = `111`. A Zernio campaign is a LinkedIn campaign group and has no Zernio id |
| Campaign | `urn:li:sponsoredCampaign:222` | `adSetId` = `222`. A Zernio ad set is a LinkedIn campaign |
| Creative | `urn:li:sponsoredCreative:333` | `adId` = Zernio's ad `_id`, or `333` once synced. `GET /v1/ads/{adId}/analytics` needs the `_id`. Find it with `GET /v1/ads?accountId=...&platformAdId=333` |
| Matched audience | `urn:li:dmpSegment:444` or `urn:li:adSegment:555` | In targeting: `audienceInclude`, `audienceExclude` take the LinkedIn id or URN. On audience endpoints: `audienceId` = Zernio's id |
| Lead form | `urn:li:leadGenForm:666` | `formId` and `leadGenFormId` = `666` |
| Conversion rule | `urn:lla:llaPartnerConversion:777` | `destinationId` = `777` |
| Insight Tag | `urn:tscp:insightTag:888` | `tagId` = the tag's partner id |
| Platform filter on ads endpoints | | `platform=linkedin` |

Campaign group and campaign endpoints find the ad account through ads Zernio has synced. A group or campaign with no synced creatives returns `404` or `409`, except on `DELETE` with `accountId`.

## Accounts

| LinkedIn Marketing API | Zernio | Send | Watch out |
|---|---|---|---|
| 3-legged OAuth | `GET /v1/connect/{platform}/ads` with `linkedin` | `profileId` | Reuses the profile's LinkedIn token when it has one, otherwise returns `authUrl` |
| `GET /rest/adAccounts?q=search`, `GET /rest/adAccountUsers?q=authenticatedUser` | `GET /v1/ads/accounts` | `accountId`, optional `adAccountId`, `limit` up to 1000, `inScopeOnly` | Live. Active ad accounts only |

## Campaign groups

| LinkedIn Marketing API | Zernio | Send | Watch out |
|---|---|---|---|
| `GET /rest/adAccounts/{id}/adCampaignGroups?q=search` | `GET /v1/ads/campaigns` | `accountId`, `adAccountId`, `platform=linkedin`, `campaignId`, `page`, `limit` up to 100. Add `live=true` for live status | Synced copy. Groups with no creatives are not listed |
| `POST /rest/adAccounts/{id}/adCampaignGroups` | `POST /v1/ads/campaigns` | `accountId`, `adAccountId`, `name`, `goal` | Created as a draft with no budget. Fill it with `POST /v1/ads/create` and `existingCampaignId` |
| Partial update `status` | `PUT /v1/ads/campaigns/{campaignId}/status` | `status`: `active` or `paused` | Returns `updated` or `skipped` |
| `BATCH_PARTIAL_UPDATE` on `status` | `POST /v1/ads/campaigns/bulk-status` | `status`, `campaigns: [{platformCampaignId, platform: "linkedin"}]`, up to 50 | |
| Partial update `name`, `totalBudget`, `dailyBudget` | `PUT /v1/ads/campaigns/{campaignId}` | `name`, `budget: {amount, type}` | A lifetime budget needs an end date on the group. A daily budget needs dynamic budget optimization on the group |
| `DELETE` (draft) or `status: PENDING_DELETION` | `DELETE /v1/ads/campaigns/{campaignId}` | `accountId` when Zernio holds no ads for it | Its ads are marked `cancelled` |
| No native copy | `POST /v1/ads/campaigns/{campaignId}/duplicate` | `statusOption` (`PAUSED`, `ACTIVE`, `INHERITED_FROM_SOURCE`), `renameStrategy`, `renamePrefix`, `renameSuffix`, `startTime`, `endTime` | Returns `copiedCampaignId`, `adSetIdMap`, `adIdMap` |

## Campaigns

| LinkedIn Marketing API | Zernio | Send | Watch out |
|---|---|---|---|
| `GET /rest/adAccounts/{id}/adCampaigns?q=search` | `GET /v1/ads/ad-sets` | `accountId`, `adAccountId`, `platform=linkedin`, `campaignId`, `page`, `limit` up to 500. Add `live=true` for live status | Synced copy, with `targeting`, `budget`, `bidAmount`, `optimizationGoal` |
| `POST /rest/adAccounts/{id}/adCampaigns` | `POST /v1/ads/create` | See [creating ads](#creating-ads) | Created together with its first creative. `POST /v1/ads/ad-sets` returns `501` |
| Partial update `status` | `PUT /v1/ads/ad-sets/{adSetId}/status` | `status`: `active` or `paused` | |
| Partial update `name`, `dailyBudget`, `totalBudget` | `PUT /v1/ads/ad-sets/{adSetId}` | `name`, `budget: {amount, type}` | |
| Partial update `targetingCriteria` | `PUT /v1/ads/{adId}` | `targeting` (see [creating ads](#creating-ads)) | Writes the ad's campaign. Merged per facet; an empty facet removes it; sending locations replaces them all |
| `DELETE` (draft) or `status: PENDING_DELETION` | `DELETE /v1/ads/ad-sets/{adSetId}` | | Its ads are marked `cancelled` |
| `GET`, `PUT /rest/adTrackingParameters/(adEntity:(sponsoredCampaign:...))` | `GET`, `PATCH /v1/ads/{adId}/tracking-tags` | `dynamicValueParameters`, `customValueParameters`; an omitted map is kept, `{}` clears it | Writes the ad's campaign. Not on conversation, message or lead form ads |

## Creating ads

`POST /v1/ads/create` replaces the chain of group, campaign, post and creative calls. Every format needs `accountId`, `adAccountId` and `budgetAmount`. It returns `ad._id`, `platformCampaignId`, `platformAdSetId` and `platformAdId`.

| What you build | Send | Watch out |
|---|---|---|
| Any new campaign | `budgetAmount`, `budgetType` (`daily` default, or `lifetime`), `goal` (`engagement`, `traffic`, `awareness`, `video_views`, `lead_generation`, `job_applicants`), optional `name`, `campaignName`, `adSetName`, `startDate`, `endDate`, `status` | `goal` is inferred when left out, except for spotlight, follower and jobs |
| Bidding | `platformSpecificData: {costType (CPM, CPC, CPV), unitCost, optimizationTargetType, audienceExpansionEnabled, offsiteDeliveryEnabled}` | `unitCost` in whole currency and needs `costType`. Unknown keys return `400` |
| Single image | `headline`, `body`, `imageUrl`, `linkUrl` for `traffic`, optional `callToAction` | Zernio uploads the image from its public URL |
| Video | `headline`, `body`, `video: {url}` | |
| Carousel | `headline`, `body`, `platformSpecificData.carousel.cards` (2 to 10, each `{imageUrl, headline, landingUrl}`) | Each card needs `landingUrl`, or set `linkUrl` |
| Document | `body`, `document: {url, title}`, `linkUrl` for `traffic` | |
| Event | `body`, `event: {urn: "urn:li:event:123"}` | |
| Text ad | `textAd: {headline (up to 25), description (up to 75), landingUrl, imageUrl}` | No `body` |
| Spotlight | `goal`, `spotlight: {headline, callToAction, landingUrl, description, logoUrl}` | `goal` `traffic` or `awareness` |
| Follower | `goal`, `follower: {headline, description, callToAction}` | `goal` `engagement` or `awareness` |
| Jobs | `goal: "job_applicants"`, `jobs: {headline, buttonLabel}` | |
| Conversation | `conversation: {subject, firstMessageId, messages: [{id, text, buttons: [{text, nextMessageId or url}]}]}` (1 to 25 messages, 1 to 5 buttons) | Guided conversations only |
| Sponsor an existing post (thought leader) | `thoughtLeader: {postUrn}` | |
| Lead gen | `goal: "lead_generation"`, `leadGenFormId` | The form attaches to image, video and document creatives only |
| New creative in an existing campaign | The format fields, plus `adSetId` | Budget, `endDate`, targeting and bidding are ignored or return `400` |
| New campaign in an existing group | The campaign fields, plus `existingCampaignId` and `goal` | Needs a budget. Not together with `adSetId` |
| Targeting | `countries` (two-letter codes), `regions: [{key: "urn:li:geo:123"}]`, `excludedLocations: {countries}`, `industries`, `companySizes`, `seniorities`, `jobFunctions`, `jobTitles`, `skills`, `interests`, `behaviors`, `audienceInclude`, `audienceExclude`, `ageMin`, `ageMax`, `gender`, `languages` | `jobTitles` cannot go with `seniorities` or `jobFunctions`. No cities, postal codes or company names. With no location LinkedIn targets the US |

## Creatives

| LinkedIn Marketing API | Zernio | Send | Watch out |
|---|---|---|---|
| `POST /rest/posts` (dark post), then `POST /rest/adAccounts/{id}/creatives` | `POST /v1/ads/create` | See [creating ads](#creating-ads) | |
| `POST /rest/adAccounts/{id}/creatives?action=createInline` | `POST /v1/ads/create` | | Image, video, carousel and document |
| `/rest/conversationAds`, `/rest/conversationAds/{id}/sponsoredMessageContents` | `POST /v1/ads/create` with `conversation` | | Guided conversations only |
| `POST /rest/images` (or `videos`, `documents`) `?action=initializeUpload` | Media URLs in `POST /v1/ads/create` | Public URLs | No standalone upload |
| Creative on an existing organization post | `POST /v1/ads/boost` | `accountId` (the posting `linkedin` account), `adAccountId`, `name`, `goal`, `platformPostId` (`urn:li:share:...` or `urn:li:ugcPost:...`), `budget: {amount, type}` | Always a new group and campaign. Organization posts only. To add one to an existing campaign, use `thoughtLeader` with `adSetId` on `POST /v1/ads/create` |
| `GET /rest/adAccounts/{id}/creatives?q=criteria` | `GET /v1/ads` | `accountId`, `adAccountId`, `platform=linkedin`, `campaignId`, `adSetId`, `platformAdId`, `page`, `limit` up to 500 | Synced copy. Returns `_id` |
| `GET /rest/adAccounts/{id}/creatives/{encoded urn}` | `GET /v1/ads/{adId}?live=true` | | Status read live, the rest from the synced copy |
| Partial update `intendedStatus` | `PUT /v1/ads/{adId}/status` | `status`: `active` or `paused` | |
| A new creative replacing the old one (LinkedIn cannot edit creative content) | `PUT /v1/ads/{adId}` | `creative: {imageUrl or videoUrl, headline, body, description, callToAction, linkUrl}` | The old creative is deleted, so `platformAdId` changes. `name` returns `501` |
| `DELETE` (draft) or `intendedStatus: PENDING_DELETION` | `DELETE /v1/ads/{adId}` | | Also removes the dark post and empty parents Zernio created |

## Targeting and forecasts

| LinkedIn Marketing API | Zernio | Send | Watch out |
|---|---|---|---|
| `GET /rest/adTargetingEntities?q=typeahead` or `q=adTargetingFacet` | `GET /v1/ads/targeting/search` | `accountId`, `q`, `dimension` (`geo`, `interest`, `behavior`, `industry`, `jobFunction`, `seniority`, `companySize`, `skill`) | No job title, company or school search |
| `GET /rest/audienceCounts?q=targetingCriteriaV2` | `POST /v1/ads/targeting/reach-estimate` | `accountId`, `adAccountId`, `spec` (the targeting fields above) | Returns `available`, `lower`, `upper` |
| `GET /rest/adBudgetPricing?q=criteriaV2` | `POST /v1/ads/targeting/bid-pricing` | `spec`, `campaignType` (`TEXT_AD`, `SPONSORED_UPDATES`, `SPONSORED_INMAILS`), `bidType`, `matchType`, `currency`, `objectiveType`, `optimizationTargetType`, `dailyBudget` | Send the ad account's `currency`; the default is USD |
| `GET /rest/adSupplyForecasts?q=criteriaV2` | `POST /v1/ads/targeting/supply-forecast` | `spec`, `timeRangeStart`, `timeRangeEnd` (future, Unix ms), `dailyBudget` or `totalBudget`, `competingBid: {bidType, amount}` or `optimizationTarget`, `objectiveType` | `objectiveType` is required by Zernio, optional at LinkedIn |

## Matched audiences

| LinkedIn Marketing API | Zernio | Send | Watch out |
|---|---|---|---|
| `GET /rest/dmpSegments?q=account`, `GET /rest/adSegments?q=accounts` | `GET /v1/ads/audiences` | `accountId`, `adAccountId` | Live. Audiences not created in Zernio have `id: null` and cannot be uploaded to or deleted |
| `POST /rest/dmpSegments`, `/rest/adPageSets`, `/rest/adSegmentSources` | `POST /v1/ads/audiences` | `accountId`, `adAccountId`, `name`, `type` (`customer_list`, `company_list`, `website_retargeting`, `engagement`); `companies` (up to 300,000), `matchRules`, or `sourceType`, `trigger`, `lookbackDays`, `engagementSources` | Returns Zernio's `id` and `platformAudienceId` |
| `dmpSegments?action=generateUploadUrl`, then `POST /rest/dmpSegments/{id}/listUploads` | `POST /v1/ads/audiences/{audienceId}/users` | `users: [{email}]`, up to 10,000 | Replaces the whole list. Company lists return `422` |
| The same flow with a company list | `POST /v1/ads/audiences/{audienceId}/companies` | `companies: [{name, domain, website, linkedinPageUrl}]` | Replaces the whole list |
| `DELETE /rest/dmpSegments/{id}` | `DELETE /v1/ads/audiences/{audienceId}` | | |

## Conversions

| LinkedIn Marketing API | Zernio | Send | Watch out |
|---|---|---|---|
| `POST /rest/conversionEvents` (single or `BATCH_CREATE`) | `POST /v1/ads/conversions` | `accountId`, `destinationId` (the rule id), `events: [{eventName, eventTime (Unix seconds), eventId, user, value, currency}]` | `eventId` is required. Sent in chunks of 5,000 |
| `GET /rest/conversions?q=account` | `GET /v1/accounts/{accountId}/conversion-destinations` | | Conversions API rules only. Insight Tag rules are under the tag's `events` |
| `POST /rest/conversions` | `POST /v1/accounts/{accountId}/conversion-destinations` | `adAccountId`, `name`, `type`, attribution windows (1, 7, 30, 90 or 365 days), `autoAssociationType`, `valueType`, `value: {currencyCode, amount}` | Not idempotent |
| `GET /rest/conversions/{id}?account=...` | `GET /v1/accounts/{accountId}/conversion-destinations/{destinationId}` | `adAccountId` | |
| Partial update `/rest/conversions/{id}?account=...` | `PATCH /v1/accounts/{accountId}/conversion-destinations/{destinationId}` | `adAccountId` and the fields to change | |
| Partial update `enabled: false` | `DELETE /v1/accounts/{accountId}/conversion-destinations/{destinationId}` | `adAccountId` | Disables the rule; LinkedIn has no hard delete |
| `PUT`, `GET`, `DELETE /rest/campaignConversions/(campaign:...,conversion:...)` | `GET`, `POST`, `DELETE /v1/accounts/{accountId}/conversion-destinations/{destinationId}/associations` | `adAccountId`, `campaignIds` (LinkedIn campaign ids, up to 100) | |
| `GET /rest/adAnalytics?q=analytics&pivot=CONVERSION` | `GET /v1/accounts/{accountId}/conversion-destinations/{destinationId}/metrics` | `adAccountId`, `startDate`, `endDate`, `granularity` | |

## Insight Tag

| LinkedIn Marketing API | Zernio | Send | Watch out |
|---|---|---|---|
| `GET /rest/insightTags?q=account` | `GET /v1/accounts/{accountId}/tracking-tags` | | |
| `POST /rest/insightTags` | `POST /v1/accounts/{accountId}/tracking-tags` | `adAccountId`, `name` | Returns the existing tag when the account has one |
| `GET /rest/insightTags/{id}`, `GET /rest/insightTagDomains?q=account` | `GET /v1/accounts/{accountId}/tracking-tags/{tagId}` | | Adds `installed` and `lastFiredTime` |
| Partial update `firstPartyTrackingEnabled` | `PATCH /v1/accounts/{accountId}/tracking-tags/{tagId}` | `firstPartyCookieStatus` | The only field you can change |
| `/rest/conversions` with Insight Tag rules | `/v1/accounts/{accountId}/tracking-tags/{tagId}/events` and `.../events/{eventId}` | `name`, `type`, `enabled`, `defaultValue`, `currency`, `clickWindowDays`, `viewWindowDays` | DELETE disables the rule |
| `GET /rest/insightTagsPermission`, `?action=grantAccess`, `?action=revokeAccess` | `GET`, `POST`, `DELETE /v1/accounts/{accountId}/tracking-tags/{tagId}/shared-accounts` | | |
| No equivalent | `/v1/accounts/{accountId}/tracking-tags/{tagId}/stats` and `.../install` | | Install the tag on Shopify or WordPress through Zernio |

## Lead gen forms

| LinkedIn Marketing API | Zernio | Send | Watch out |
|---|---|---|---|
| `POST /rest/leadForms` | `POST /v1/ads/lead-forms` | `accountId`, `name`, `privacyPolicyUrl`, `platformSpecificData: {adAccountId, headline, description, questions, consents, locale, state}` | Text and multiple-choice questions. Returns `form.id` |
| `GET /rest/leadForms?q=owner` | `GET /v1/ads/lead-forms` | `accountId`, `adAccountId` | |
| `GET /rest/leadForms/{id}` | `GET /v1/ads/lead-forms/{formId}` | `accountId` | |
| Partial update `state: ARCHIVED` | `DELETE /v1/ads/lead-forms/{formId}` | `accountId` | Archives the form |
| `GET /rest/leadFormResponses?q=owner&leadType=(leadType:SPONSORED)` | `GET /v1/ads/leads` | `accountId`, `adAccountId`, `platform=linkedin`, `formId`, `since` (Unix seconds), `limit` up to 100, `cursor` | Read live on every call. LinkedIn keeps leads 90 days |

## Analytics

| LinkedIn Marketing API | Zernio | Send | Watch out |
|---|---|---|---|
| `GET /rest/adAnalytics?q=analytics&pivot=CREATIVE` | `GET /v1/ads/{adId}/analytics` | `fromDate`, `toDate` | `adId` must be Zernio's `_id`. Daily series |
| `adAnalytics` with a `MEMBER_*` pivot | `GET /v1/ads/{adId}/analytics` | `breakdowns`: `job_title`, `job_function`, `seniority`, `industry`, `company`, `company_size`, `country`, `region` | Totals for the range, not daily |
| `adAnalytics` filtered to a campaign group | `GET /v1/ads/campaigns/{campaignId}/analytics` | `fromDate`, `toDate`, `platform`, `breakdowns` | |
| `adAnalytics` across the account | `GET /v1/ads/performance` | `accountId`, `fromDate`, `toDate` (both required), `adAccountId`, `platform=linkedin`, `sortBy`, `limit` up to 100 | Synced store; 90 days imported at connect |

## Ad library

| LinkedIn | Zernio | Send | Watch out |
|---|---|---|---|
| LinkedIn Ad Library search | `GET /v1/ads/library` | `accountId` (a LinkedIn connection), `q` or `advertiser`, `countries`, `since`, `until`, `limit` up to 25, `after` | `accountId` picks LinkedIn; without it the search runs on Meta |

## No Zernio equivalent

Keep these on the LinkedIn Marketing API, or drop them:

- **Accounts:** creating or editing ad accounts (`POST /rest/adAccounts`), and `adAccountUsers` roles (`PUT`, `DELETE /rest/adAccountUsers/...`).
- **Batches:** `BATCH_CREATE`, `BATCH_GET` and `BATCH_PARTIAL_UPDATE` on groups, campaigns and creatives, beyond bulk status.
- **Campaigns:** creating a campaign without a creative, duplicating one campaign or one creative, reactivating a `COMPLETED` campaign, `DRAFT` objects.
- **Targeting:** `GET /rest/adTargetingFacets`, `adTargetingEntities?q=similarEntities` and `q=urns`, job title, company and school search, city and postal code targeting.
- **Creatives:** classic single-message ads, standalone image, video and document uploads, previews, comments on sponsored posts, lead forms on carousel and other formats.
- **Audiences:** streaming users or companies with `ADD` and `REMOVE` (`/rest/dmpSegments/{id}/users`), removing users, renaming an audience, predictive audiences.
- **Reporting:** `adAnalytics?q=statistics`, `q=attributedRevenueMetrics`, and pivots other than creative, campaign group, conversion and the member pivots above.
- **Leads:** `leadNotifications` webhooks and test leads. Poll `GET /v1/ads/leads` instead.
- **Tracking:** account-level `adTrackingParameters`, and editing blocked domains on `insightTagDomains`.