Lesson 05 · outer fringe

Five Vocabularies, One Ad

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


Where the confusion comes from

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.

The five families

Learn these as five separate namespaces. A token belonging to one is almost never valid in another, even when the strings look similar.

FieldAnswersSample 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.

type and format are not redundant

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.

The objectiveType enum is not trustworthy

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 fields are one table, not four

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.

objectiveTypeformatoptimizationTargetTypecostTypeLANConv. tracking
BRAND_AWARENESSSINGLE_VIDEOMAX_REACHCPMoptionaloptional
BRAND_AWARENESSFOLLOW_COMPANYNONECPMoptionalREQUIRED
WEBSITE_CONVERSIONSPOTLIGHTNONECPCdisallowedREQUIRED
LEAD_GENERATIONSPONSORED_INMAILNONECPMdisallowedoptional
VIDEO_VIEWSINGLE_VIDEOMAX_VIDEO_VIEWCPVoptionaloptional

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.


Practice

Exercise 1 · recognition

Which namespace does this token live in?

Interleaved on purpose — these are exactly the tokens that blur together.

Exercise 2 · bug hunt

Find the invalid combination

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

From memory

Fill in without scrolling up. Case-insensitive.

Exercise 4 · execution

Name the row

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.


Retaining this

  1. Five namespaces, one sentence each. type = machine, format = look, objectiveType = what you buy, optimizationTargetType+costType = how you pay, creativeType = read-only echo. If you can say that unprompted tomorrow, the lesson landed.
  2. The habit, not the table. When you meet an unfamiliar enum value in these docs, your first move is to check whether the schema table and the worked example agree. Here they don't, twice. That check costs ten seconds and it is the difference between a validated integration and a guessed one.
  3. Tonight or tomorrow: write out the four type values from memory, then for each name one format that belongs to it. Then name the one format whose spelling differs from its type for no reason. That last question is the whole naming mess in miniature.

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.