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/accountslists. - 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 | 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.
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" }
}'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 campaignimport 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:
headlineis required, plus exactly one ofimageUrlorvideo: { url }.bodyis the intro text.organizationIdnames the authoring Page, as a number orurn:li:organization:N. It is optional when the Page is connected to Pozy or owns the ad account.goal: "traffic"needslinkUrl. Buttons acceptLEARN_MORE,SIGN_UP,DOWNLOAD,SUBSCRIBE,REGISTER,JOIN,ATTEND,REQUEST_DEMO,VIEW_QUOTE,APPLY,SEE_MORE,SHOP_NOWandBUY_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) |
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 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.
#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 and 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. |