Lesson 05 · outer fringe
A single campaign object carries five overlapping enum families that all sound like they mean "what kind of ad is this". They do not. This is the densest naming mess in the whole API, and getting it wrong is a 400 at best and the wrong ad at worst.
≈15 min · pinned to li-lms-2026-08
In Meta's model, the three questions — what am I buying, who am I targeting, what does the ad look like — live on three different objects: Campaign, Ad Set, Ad. LinkedIn has no Ad Set. Lesson 01 established that the spine is four levels, not five, and that the missing level's responsibilities got pushed up onto Campaign.
This lesson is the bill for that. Everything Meta spreads over two objects, LinkedIn crams into one — and because the fields arrived in different years, they were named by different people who did not talk to each other.
Learn these as five separate namespaces. A token belonging to one is almost never valid in another, even when the strings look similar.
| Field | Answers | Sample values |
|---|---|---|
| type CampaignType |
Which delivery machine. The oldest, coarsest layer. Four values, ever. | TEXT_AD SPONSORED_UPDATES SPONSORED_INMAILS DYNAMIC |
| format CampaignFormat |
What the ad looks like. The real discriminator. Grows every few months. | STANDARD_UPDATE CAROUSEL SINGLE_VIDEO SPOTLIGHT SPONSORED_MESSAGE |
| objectiveType | What you are buying. Seven-ish, and the values are unstable — see below. | BRAND_AWARENESS WEBSITE_VISIT LEAD_GENERATION ENGAGEMENT |
| optimizationTargetType + costType |
How LinkedIn spends the budget, and what a charged unit is. | MAX_CLICK · MAX_LEAD TARGET_COST_PER_CLICK CPM · CPC · CPV |
| creativeType | Nothing you write. A read-side vocabulary used by Campaign Manager and reporting. | SPONSORED_STATUS_UPDATE SPOTLIGHT_V2 JOBS_V2 |
The one that catches everybody
creativeType is not a field you set. It is not on the campaign and it is not on the creative you POST. It appears in the objectives-mapping docs and in reporting output, and it shadows format with near-identical names — SPOTLIGHT (format) vs SPOTLIGHT_V2 (creativeType), CAROUSEL vs SPONSORED_UPDATE_CAROUSEL. If you find yourself typing a _V2 suffix into a write payload, you have crossed namespaces.
The instinct is that format makes type obsolete. It doesn't. type selects a delivery machine with genuinely different rules attached, and the API validates the pair.
graph LR T1["type: SPONSORED_UPDATES"] --> F1["STANDARD_UPDATE"] T1 --> F2["CAROUSEL"] T1 --> F3["SINGLE_VIDEO"] T1 --> F4["SPONSORED_UPDATE_EVENT"] T1 --> F5["SPONSORED_UPDATE_
NATIVE_DOCUMENT"] T2["type: DYNAMIC"] --> F6["SPOTLIGHT"] T2 --> F7["FOLLOW_COMPANY"] T2 --> F8["JOBS"] T3["type: SPONSORED_INMAILS"] --> F9["SPONSORED_INMAIL"] T3 --> F10["SPONSORED_MESSAGE"] T4["type: TEXT_AD"] --> F11["TEXT"] classDef t fill:#dbeafe,stroke:#2563eb,color:#1e3a5f classDef f fill:#f5f5f4,stroke:#a8a29e,color:#44403c class T1,T2,T3,T4 t class F1,F2,F3,F4,F5,F6,F7,F8,F9,F10,F11 f
Three things to notice, each of which will bite you.
1. TEXT_AD is a type; TEXT is the format. Same product, two spellings, two fields. There is no reason for this beyond history.
2. Sponsored Messaging has two formats, not one. SPONSORED_INMAIL is a single message; SPONSORED_MESSAGE is a Conversation Ad — a branching tree of replies. They share a type and diverge sharply in their content model. That divergence is a later lesson; for now just refuse to treat them as synonyms.
3. DYNAMIC tightens the rules. For type=DYNAMIC the docs state format is required (it is optional otherwise), all creatives under the campaign must share one creative type, and both dailyBudget and totalBudget must be set — where every other campaign type needs only one of the two.
This is worth your attention because it is the kind of thing you cannot discover by reading carefully — only by noticing a contradiction.
The campaign schema table lists these seven, plural:
BRAND_AWARENESS ENGAGEMENT JOB_APPLICANTS LEAD_GENERATION
WEBSITE_CONVERSIONS WEBSITE_VISITS VIDEO_VIEWS
Every worked example on the same page, and the entire Campaign Objectives mapping page, use the singular:
JOB_APPLICANT WEBSITE_CONVERSION WEBSITE_VISIT VIDEO_VIEW
// and, only in the Dynamic Ads section: WEBSITE_TRAFFIC
// and, only on the objectives page: TALENT_LEAD
What to actually do
Do not hand-transcribe this enum into your codebase from the schema table. The examples and the Postman collection reflect the wire format; the prose table does not. Two consequences for your design:
The last structural idea, and the one that should change your schema. These four fields are not independent. LinkedIn documents them as a validation matrix: (objectiveType, format) determines which optimizationTargetType values are legal, which in turn pins costType, and separately gates three booleans.
| objectiveType | format | optimizationTargetType | costType | LAN | Conv. tracking |
|---|---|---|---|---|---|
| BRAND_AWARENESS | SINGLE_VIDEO | MAX_REACH | CPM | optional | optional |
| BRAND_AWARENESS | FOLLOW_COMPANY | NONE | CPM | optional | REQUIRED |
| WEBSITE_CONVERSION | SPOTLIGHT | NONE | CPC | disallowed | REQUIRED |
| LEAD_GENERATION | SPONSORED_INMAIL | NONE | CPM | disallowed | optional |
| VIDEO_VIEW | SINGLE_VIDEO | MAX_VIDEO_VIEW | CPV | optional | optional |
Five rows out of a matrix with dozens. Do not memorise it. Memorise the shape: a campaign's delivery configuration is one row of a lookup table, and the row is chosen by the objective/format pair.
In your own schema, that argues strongly for a single discriminated union keyed on objective, rather than five nullable columns:
// Five loose columns: every invalid combination is representable.
type Bad = {
objective: string; format: string;
optimizationTarget: string | null; costType: string | null;
}
// One row of the matrix, chosen by the objective. Illegal states won't compile.
type Delivery =
| { objective: 'BRAND_AWARENESS'; format: 'SINGLE_VIDEO' | 'STANDARD_UPDATE';
bid: { kind: 'auto'; target: 'MAX_REACH' | 'MAX_IMPRESSION'; costType: 'CPM' } }
| { objective: 'VIDEO_VIEW'; format: 'SINGLE_VIDEO';
bid: { kind: 'auto'; target: 'MAX_VIDEO_VIEW'; costType: 'CPV' }
| { kind: 'manual'; costType: 'CPM'; unitCost: Money } }
| { objective: 'LEAD_GENERATION'; format: LeadGenFormat;
bid: { kind: 'auto'; target: 'MAX_LEAD' | 'MAX_QUALIFIED_LEAD'; costType: 'CPM' }
| { kind: 'manual'; costType: 'CPC'; unitCost: Money };
lanAllowed: false }
The second version is more typing and it will save you a support rotation. The lanAllowed: false on lead gen is the tell: constraints that live only in a docs table will eventually live in someone's head, and heads leave.
One live contradiction, flagged
The two docs pages disagree about lead gen formats. The campaigns page's validation table lists STANDARD_UPDATE, CAROUSEL, SINGLE_VIDEO under Lead Generation; the objectives page lists a distinct family — LEAD_GENERATION_FORM_SPONSORED_CONTENT, LEAD_GENERATION_FORM_CAROUSEL_SPONSORED_CONTENT, VIDEO_LEAD_GENERATION_FORM_SPONSORED_CONTENT, LEAD_GENERATION_FORM_SPONSORED_INMAIL. Both are current as of li-lms-2026-08. Resolve it against the API before building the lead gen path; do not pick one on the basis of which page you read last.
Exercise 1 · recognition
Interleaved on purpose — these are exactly the tokens that blur together.
Exercise 2 · bug hunt
Three POST /adAccounts/{id}/adCampaigns bodies, trimmed to the fields that matter. Click the offending line, or say there is no bug. One of them is clean.
A · intent: "a brand awareness video campaign optimised for reach"
{ "type": "SPONSORED_UPDATES", "format": "SINGLE_VIDEO", "objectiveType": "BRAND_AWARENESS", "optimizationTargetType": "MAX_REACH", "costType": "CPC", "unitCost": { "amount": "0", "currencyCode": "EUR" }}
B · intent: "a Spotlight ad driving traffic to a landing page"
{ "type": "SPONSORED_UPDATES", "format": "SPOTLIGHT", "objectiveType": "WEBSITE_VISIT", "optimizationTargetType": "NONE", "costType": "CPC", "offsiteDeliveryEnabled": false}
C · intent: "a Conversation Ad for lead generation"
{ "type": "SPONSORED_INMAILS", "format": "SPONSORED_MESSAGE", "objectiveType": "LEAD_GENERATION", "creativeSelection": "OPTIMIZED", "costType": "CPM", "offsiteDeliveryEnabled": false}
Exercise 3 · recall
Fill in without scrolling up. Case-insensitive.
Exercise 4 · execution
A customer says: "I want to promote our new product video and I only care about how many people watch it. Charge me per view." Write the four delivery fields — type, format, objectiveType, optimizationTargetType, costType — and one sentence on what you'd have to refuse them.
Now unlocked
You have configured a campaign correctly. Next: it still isn't running. Lesson 06 — Why Isn't It Serving? — covers the vocabulary of intent versus effect: status, intendedStatus, servingStatuses, servingHoldReasons, isServing. Five fields, and no two entities in the hierarchy expose the same subset.
Sources: Create and Manage Campaigns (campaign schema; optimization target types; validations by objective), Campaign Objectives (the objective → format → creativeType → bidding mapping), Advertising Overview. Version li-lms-2026-08.