# LinkedIn Ads

Create and boost LinkedIn campaigns with B2B targeting, every creative format, matched audiences, bid and delivery forecasts and the public ad library.

A `linkedinads` connection runs LinkedIn's Marketing API. You can create sponsored content from scratch in any of LinkedIn's formats, boost Company Page posts, target by industry, company size, seniority, job function, title and skill, build matched audiences, forecast delivery before you spend, and read firmographic breakdowns of who saw an ad.

## Before you start

Three things have to be in place on LinkedIn:

- An ad account. Pozy uses its numeric sponsored account id, which `GET /v1/ads/accounts` lists.
- A Company Page linked to that ad account. New ads go out in the Page's name as Direct Sponsored Content, so they do not show up in the Page's own feed.
- A Page role for whoever connected LinkedIn: Administrator, or Direct Sponsored Content Poster. Without one of them LinkedIn refuses with `403`.

LinkedIn reviews every ad, so new campaigns sit in `DRAFT` and review before they deliver.

## Connect

Call `GET /v1/connect/linkedin/ads` with the `profileId`. When the profile already has a connected LinkedIn account, the ads connection reuses its token and no consent screen appears. Otherwise the response carries an `authUrl`.

The standard LinkedIn connection asks for every ads permission: reading ad accounts, managing campaigns, reporting, conversions and lead form responses. An account connected before one of these was requested must reconnect. Missing conversion access returns `403` with `linkedin_reconnect_required`; missing lead access returns `400` with `ads_connection_required`.

## How LinkedIn's levels map

| Pozy | LinkedIn | Id on the created ad |
| --- | --- | --- |
| Campaign | Campaign group | `platformCampaignId` |
| Ad set (`adSetId`) | Campaign: bidding, targeting, schedule, budget | `platformAdSetId` |
| Ad | Creative | `platformAdId` |

## Create a campaign

`POST /v1/ads/create` creates the Direct Sponsored Content post, then the campaign group, campaign and creative around it.

```bash cURL
curl -X POST https://api.pozy.ai/v1/ads/create \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: li-ops-guide-q4" \
  -d '{
    "accountId": "66b2e19d8c3f5a7e9d0b1c5a",
    "adAccountId": "512345678",
    "organizationId": "10293847",
    "name": "Ops leaders - guide download",
    "goal": "traffic",
    "budgetAmount": 60,
    "budgetType": "daily",
    "headline": "The 2027 field ops playbook",
    "body": "How 40 operations teams cut dispatch time by a third. Free guide.",
    "imageUrl": "https://cdn.example.com/ops-guide-1200x627.png",
    "linkUrl": "https://example.com/guide",
    "callToAction": "DOWNLOAD",
    "status": "PAUSED",
    "targeting": {
      "countries": ["US", "CA"],
      "seniorities": ["6", "7"],
      "jobFunctions": ["18"],
      "companySizes": ["D", "E", "F"]
    },
    "platformSpecificData": { "costType": "CPC" }
  }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/ads/create", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Idempotency-Key": "li-ops-guide-q4",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    accountId: "66b2e19d8c3f5a7e9d0b1c5a",
    adAccountId: "512345678",
    organizationId: "10293847",
    name: "Ops leaders - guide download",
    goal: "traffic",
    budgetAmount: 60,
    budgetType: "daily",
    headline: "The 2027 field ops playbook",
    body: "How 40 operations teams cut dispatch time by a third. Free guide.",
    imageUrl: "https://cdn.example.com/ops-guide-1200x627.png",
    linkUrl: "https://example.com/guide",
    callToAction: "DOWNLOAD",
    status: "PAUSED",
    targeting: { countries: ["US", "CA"], seniorities: ["6", "7"], jobFunctions: ["18"], companySizes: ["D", "E", "F"] },
    platformSpecificData: { costType: "CPC" },
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.ad.platformAdSetId); // the LinkedIn campaign
```
```python Python
import os, requests

resp = requests.post(
    "https://api.pozy.ai/v1/ads/create",
    headers={
        "Authorization": f"Bearer {os.environ['POZY_API_KEY']}",
        "Idempotency-Key": "li-ops-guide-q4",
    },
    json={
        "accountId": "66b2e19d8c3f5a7e9d0b1c5a",
        "adAccountId": "512345678",
        "organizationId": "10293847",
        "name": "Ops leaders - guide download",
        "goal": "traffic",
        "budgetAmount": 60,
        "budgetType": "daily",
        "headline": "The 2027 field ops playbook",
        "body": "How 40 operations teams cut dispatch time by a third. Free guide.",
        "imageUrl": "https://cdn.example.com/ops-guide-1200x627.png",
        "linkUrl": "https://example.com/guide",
        "callToAction": "DOWNLOAD",
        "status": "PAUSED",
        "targeting": {"countries": ["US", "CA"], "seniorities": ["6", "7"], "jobFunctions": ["18"], "companySizes": ["D", "E", "F"]},
        "platformSpecificData": {"costType": "CPC"},
    },
    timeout=60,
)
resp.raise_for_status()
print(resp.json()["ad"]["_id"])
```

Field rules:

- `headline` is required, plus exactly one of `imageUrl` or `video: { url }`. `body` is the intro text.
- `organizationId` names the authoring Page, as a number or `urn:li:organization:N`. It is optional when the Page is connected to Pozy or owns the ad account.
- `goal: "traffic"` needs `linkUrl`. Buttons accept `LEARN_MORE`, `SIGN_UP`, `DOWNLOAD`, `SUBSCRIBE`, `REGISTER`, `JOIN`, `ATTEND`, `REQUEST_DEMO`, `VIEW_QUOTE`, `APPLY`, `SEE_MORE`, `SHOP_NOW` and `BUY_NOW`.
- A video upload blocks while LinkedIn transcodes it, usually 10 to 30 seconds.
- `status: "PAUSED"` pauses only the top level the call creates: a new campaign group, a new campaign (kept in draft) or the new creative.

### Goals

| `goal` | On create | On boost | Notes |
| --- | --- | --- | --- |
| `engagement`, `traffic`, `awareness` | Yes | Yes | |
| `lead_generation` | Yes | Yes | Needs `leadGenFormId` ([Lead forms](https://pozy.ai/docs/ads/lead-forms)) |
| `video_views` | Yes | Yes | Needs a video |
| `job_applicants` | Yes | – | Needs the `jobs` format |
| `conversions` | – | Yes | Boost only |

### Reuse an existing campaign or group

Send `adSetId` (a LinkedIn campaign id) to add a new creative under that campaign: bidding, targeting, schedule and budget are inherited, so sending any of them returns `400`. Send `existingCampaignId` (a campaign group id) to create a new campaign in that group with its own settings. The two are mutually exclusive. If a create fails, Pozy cleans up only what it created and never touches the parent.

## Creative formats

One key inside `platformSpecificData` selects the format. Formats exclude each other and the top-level `imageUrl` and `video`.

| Format | Key | Goals | Notes |
| --- | --- | --- | --- |
| Single image or link (article, newsletter) | none | engagement, traffic, awareness, lead_generation | `imageUrl` + `linkUrl` |
| Single video | none (`video`) | all base goals plus video_views | MP4 H.264/AAC, 3 s to 30 min, 75 KB to 500 MB |
| Carousel | `carousel` | base goals | 2 to 10 cards, each with a landing URL (or the ad's `linkUrl`) |
| Document | `document` | base goals | PDF, PPT or DOC up to 100 MB, swipeable in the feed |
| Event | `event` | base goals | Promotes an existing LinkedIn Event by `urn` |
| Text ad | `textAd` | base goals | Right rail. Headline 25 characters, description 75, optional 100 x 100 logo |
| Spotlight | `spotlight` | traffic, awareness | Personalized with the viewer's photo |
| Follower | `follower` | engagement, awareness | Drives Page follows |
| Jobs | `jobs` | job_applicants | Shows the Page's live job postings |
| Conversation | `conversation` | base goals | A message tree in LinkedIn messaging: up to 25 messages, 5 buttons each, 500 characters per message |
| Thought leader | `thoughtLeader` | base goals | Sponsors an existing Company Page post (`urn:li:share:N` or `urn:li:ugcPost:N`) under a campaign you control |

Conversation ads need a sender approved for the ad account in Campaign Manager, or they fail with `SINMAIL_SENDER_NOT_APPROVED`. Thought leader and boost only work on posts authored by the Company Page; LinkedIn rejects personal-profile posts.

## Bidding and budgets

Bidding fields go in `platformSpecificData` on create and boost:

| Field | Values |
| --- | --- |
| `costType` | `CPM` (default), `CPC`, `CPV`. Required with `unitCost`. |
| `unitCost` | Manual bid in whole currency units. Omit for automated bidding. |
| `optimizationTargetType` | LinkedIn's value, such as `MAX_CLICK` or `TARGET_COST_PER_CLICK` |
| `creativeSelection` | `OPTIMIZED` (default) or `ROUND_ROBIN` |
| `audienceExpansionEnabled`, `offsiteDeliveryEnabled`, `connectedTelevisionOnly` | Switches for where and to whom the ads deliver |

LinkedIn requires at least $10 a day per campaign, and $100 lifetime on an inactive campaign. Bidding cannot be changed after create through Pozy.

### Forecast before you spend

| Call | Answers |
| --- | --- |
| `POST /v1/ads/targeting/bid-pricing` | The suggested bid and the bid and daily budget LinkedIn accepts for a targeting `spec`. `dailyBudgetLimits.min` is the real minimum for that audience. |
| `POST /v1/ads/targeting/supply-forecast` | Projected impressions, clicks and spend over a future window (`timeRangeStart`, `timeRangeEnd` in Unix milliseconds) for a `dailyBudget` or `totalBudget` |
| `POST /v1/ads/targeting/reach-estimate` | Audience size for the spec |

Send `objectiveType` (for example `WEBSITE_VISIT`) on the forecast: LinkedIn rejects it without one. Read each series by its `metricType`; the `CUSTOM` granularity is the total over your window. On accounts that are not LinkedIn, the planning calls return `{ "available": false }` with `200`, so a multi-network UI can call them unconditionally.

## Targeting

LinkedIn targets by location and professional facets. Send them in a nested `targeting` object:

| Field | Values |
| --- | --- |
| `countries`, `regions` | ISO codes, or `urn:li:geo:*` keys from the search |
| `industries`, `seniorities`, `jobFunctions`, `jobTitles`, `skills` | Id fragments or full URNs from the search |
| `companySizes` | `A` (1), `B` (2 to 10), `C` (11 to 50), `D` (51 to 200), `E` (201 to 500), `F` (501 to 1,000), `G` (1,001 to 5,000), `H` (5,001 to 10,000), `I` (10,001+) |
| `interests`, `behaviors` | `[{ "id": "urn:..." }]` from the search |
| `audienceInclude`, `audienceExclude` | Matched audience segment ids |
| `ageMin`, `ageMax`, `gender`, `languages` | Rounded to LinkedIn's age buckets (18-24, 25-34, 35-54, 55+) |

`jobTitles` cannot be combined with `seniorities` or `jobFunctions`. Cities, ZIP codes, radius and income are not available on LinkedIn. `PUT /v1/ads/{adId}` with `targeting` updates the live campaign facet by facet. See [Targeting](https://pozy.ai/docs/ads/targeting#linkedin) for the lookup dimensions.

## Matched audiences

`POST /v1/ads/audiences` builds four LinkedIn audience types: contact lists (`customer_list`), company lists (`company_list`, up to 300,000 rows), engagement retargeting (`engagement`) and website retargeting (`website_retargeting`, fed by the Insight Tag). Contact list uploads replace the list rather than adding to it. Details and limits are on [Audiences](https://pozy.ai/docs/ads/audiences#audience-types).

## Lead gen forms and conversions

LinkedIn lead forms are created, listed and archived through the lead form endpoints, and responses are kept for 90 days. LinkedIn sends no lead webhook, so poll for new leads. Server-side conversions use conversion rules. Both are covered on [Lead forms](https://pozy.ai/docs/ads/lead-forms) and [Conversions](https://pozy.ai/docs/ads/conversions).

## Manage running ads

| Job | Call | Notes |
| --- | --- | --- |
| Pause or resume | `PUT /v1/ads/campaigns/{campaignId}/status` (group), `/ad-sets/{adSetId}/status` (campaign), `/{adId}/status` (creative) | A creative still in `DRAFT` cannot be paused before approval. |
| Change the budget | `PUT /v1/ads/{adId}` with `budget` | The budget belongs to the LinkedIn campaign that holds the ad, so every creative in that campaign shares it. |
| Swap media | `PUT /v1/ads/{adId}` with `creative` and a new `imageUrl` or `videoUrl` | Pozy creates a new creative and pauses the old one. Text-only edits return `400`. |
| Rename | `PUT /v1/ads/ad-sets/{adSetId}` with `name` | Creatives cannot be renamed. |
| Duplicate | `POST /v1/ads/campaigns/{campaignId}/duplicate` with `platform: "linkedin"` | Copies the group, its campaigns and creatives. `statusOption` defaults to `PAUSED`. |
| Delete | `DELETE` on the ad, ad set or campaign | Campaigns and groups move to `PENDING_DELETION` and stay readable for reporting. |

## Reporting

Spend, impressions, clicks, CPC and CPM roll up on `GET /v1/ads/tree`. Firmographic breakdowns come from `GET /v1/ads/{adId}/analytics` or `GET /v1/ads/campaigns/{campaignId}/analytics` with `breakdowns`:

| Dimension | Shows |
| --- | --- |
| `job_title`, `job_function`, `seniority` | Who the viewers are |
| `industry`, `company`, `company_size` | Where they work |
| `country`, `region` | Where they are |

Each row carries LinkedIn's URN in `value` and a readable `name`. Breakdowns cover the whole range (LinkedIn does not split them by day), lag 12 to 24 hours and omit segments with fewer than 3 events. Each dimension costs one extra call to LinkedIn, so ask only for the ones you show.

## Ad library

`GET /v1/ads/library?platform=linkedin&accountId=...&q=...` searches LinkedIn's public archive of ads served since June 2023, by keyword (`q`), `advertiser`, `countries` and a `since` / `until` window. Pages hold at most 25 ads. EU ads also carry impression ranges and targeting disclosures.

## Limits and errors

| Error | Cause | Fix |
| --- | --- | --- |
| `403` with LinkedIn's message | The connected person has no suitable Page role, or the Page is not linked to the ad account | Grant the role, or link the Page to the ad account, on LinkedIn. |
| `400` with both `adSetId` and `existingCampaignId` | They exclude each other | Send one. |
| `400` on `platformSpecificData` | An unknown key, or two format keys | Send one format key and the bidding keys above. |
| `400` `mutually_exclusive_fields` | `jobTitles` with `seniorities` or `jobFunctions` | Keep one side. |
| `422` with `INVALID_URN_TYPE` | Boosting a personal-profile post | Boost a post the Company Page authored. |
| `422` mentioning sponsorship authorization | The Page post is not approved for sponsorship | The author approves it in LinkedIn's sponsorship settings. |
| `501` on `name` for an ad | Creatives cannot be renamed | Rename the campaign instead. |

## Endpoints

| Endpoint | What it does |
| --- | --- |
| [`GET /v1/connect/{platform}/ads`](https://pozy.ai/docs/api/connectAds) | Connect ads for a platform |
| [`GET /v1/ads/accounts`](https://pozy.ai/docs/api/listAdAccounts) | List ad accounts |
| [`POST /v1/ads/create`](https://pozy.ai/docs/api/createStandaloneAd) | Create standalone ad |
| [`POST /v1/ads/boost`](https://pozy.ai/docs/api/boostPost) | Boost post as ad |
| [`GET /v1/ads/tree`](https://pozy.ai/docs/api/getAdTree) | Get campaign tree |
| [`GET /v1/ads/{adId}`](https://pozy.ai/docs/api/getAd) | Get ad details |
| [`PUT /v1/ads/{adId}`](https://pozy.ai/docs/api/updateAd) | Update ad |
| [`PUT /v1/ads/{adId}/status`](https://pozy.ai/docs/api/updateAdStatus) | Pause or resume a single ad |
| [`PUT /v1/ads/ad-sets/{adSetId}`](https://pozy.ai/docs/api/updateAdSet) | Update an ad set |
| [`PUT /v1/ads/ad-sets/{adSetId}/status`](https://pozy.ai/docs/api/updateAdSetStatus) | Pause or resume a single ad set |
| [`PUT /v1/ads/campaigns/{campaignId}/status`](https://pozy.ai/docs/api/updateAdCampaignStatus) | Pause or resume a campaign |
| [`POST /v1/ads/campaigns/{campaignId}/duplicate`](https://pozy.ai/docs/api/duplicateAdCampaign) | Duplicate a campaign |
| [`DELETE /v1/ads/{adId}`](https://pozy.ai/docs/api/deleteAd) | Cancel an ad |
| [`DELETE /v1/ads/ad-sets/{adSetId}`](https://pozy.ai/docs/api/deleteAdSet) | Delete an ad set |
| [`DELETE /v1/ads/campaigns/{campaignId}`](https://pozy.ai/docs/api/deleteAdCampaign) | Delete a campaign |
| [`GET /v1/ads/targeting/search`](https://pozy.ai/docs/api/searchAdTargeting) | Search targeting options |
| [`POST /v1/ads/targeting/reach-estimate`](https://pozy.ai/docs/api/estimateAdReach) | Estimate audience reach |
| [`POST /v1/ads/targeting/bid-pricing`](https://pozy.ai/docs/api/getLinkedInBidPricing) | Suggested bid and budget bounds |
| [`POST /v1/ads/targeting/supply-forecast`](https://pozy.ai/docs/api/getLinkedInSupplyForecast) | Forecast ad delivery |
| [`POST /v1/ads/audiences`](https://pozy.ai/docs/api/createAdAudience) | Create custom audience |
| [`POST /v1/ads/audiences/{audienceId}/users`](https://pozy.ai/docs/api/addUsersToAdAudience) | Add users to audience |
| [`POST /v1/ads/audiences/{audienceId}/companies`](https://pozy.ai/docs/api/replaceAdAudienceCompanies) | Replace audience companies |
| [`GET /v1/ads/audiences`](https://pozy.ai/docs/api/listAdAudiences) | List custom audiences |
| [`GET /v1/ads/{adId}/analytics`](https://pozy.ai/docs/api/getAdAnalytics) | Get ad analytics |
| [`GET /v1/ads/campaigns/{campaignId}/analytics`](https://pozy.ai/docs/api/getCampaignAnalytics) | Get campaign analytics |
| [`GET /v1/ads/library`](https://pozy.ai/docs/api/searchAdLibrary) | Search the public Ad Library |
