Lesson 01 · inner fringe
Four objects, their exact names, and the one cardinality rule that will bite your schema if you get it wrong.
≈12 min · pinned to li-lms-2026-08
Almost every confusion in the LinkedIn Ads API comes from collapsing two separate structures into one mental picture. There is a spend spine — the strictly hierarchical chain that answers whose money, how much, aimed at whom. And there is a content graph — the loose, cross-referenced web of posts, videos, events, and organization pages that answers what actually appears on screen, and who is seen to have said it.
The spine is a tree. The content graph is not. They meet at exactly one field, on the lowest node of the spine. This lesson is the spine only; the content graph is Lesson 02.
Learn all three columns together. In this API the human name, the REST resource, and the URN namespace are all different strings, and you will need each in a different place — the resource name in your HTTP client, the URN in every payload, the human name in conversation with a customer.
| Human name | REST resource | URN namespace | Owns |
|---|---|---|---|
| Ad Account | /adAccounts | urn:li:sponsoredAccount | Currency, billing, who may touch it, which entity it advertises for |
| Campaign Group | /adCampaignGroups | urn:li:sponsoredCampaignGroup | A shared status, budget and schedule ceiling over related campaigns |
| Campaign | /adCampaigns | urn:li:sponsoredCampaign | Almost everything: objective, targeting, budget, bid, schedule, locale, format, placement |
| Creative | /adCreatives | urn:li:sponsoredCreative | A pointer to the thing to render, plus a call-to-action and a review verdict |
Note the prefix asymmetry: the resources are ad*, the URNs are sponsored*. This is not a typo in the docs — it is two generations of naming that were never reconciled. Bake both into your constants file and stop thinking about it.
The child resources are nested under the account, so the account ID appears twice in a typical write — once in the path, once implicitly as the parent:
POST /rest/adAccounts/{accountId}/adCampaignGroups
POST /rest/adAccounts/{accountId}/adCampaigns
POST /rest/adAccounts/{accountId}/adCreatives
A URN (Uniform Resource Name) is LinkedIn's universal typed pointer. Three colon-separated parts after the scheme:
urn:li:sponsoredCampaign:123456789
│ │ │ └─ opaque id, assigned by LinkedIn
│ │ └─ namespace = the type
│ └─ namespace authority: LinkedIn
└─ scheme
The payoff is that the type travels with the reference. A field typed as "a URN" can accept a heterogeneous union, and the server can dispatch on the namespace. You will see exactly this in Lesson 02, where one content field accepts six different namespaces.
Correcting your calibration answer
You reasoned that URNs remove the need for an incrementing counter and might let the client generate IDs. Half right, and the half that's wrong matters. The namespace does remove the need for a single global ID space — sponsoredCampaign:5 and organization:5 coexist without collision. But LinkedIn always mints the ID server-side. On create you get it back in the x-restli-id response header, not in the body. There is no client-supplied idempotency key, which is precisely why your integration needs its own request-dedupe layer.
You already think in Meta's model. Keep it — but the word Campaign denotes a different level in each system, and that is the single most expensive confusion available to you here.
| Meta | Nearest LinkedIn | Where the analogy breaks |
|---|---|---|
| Ad Account | Ad Account | Clean match. LinkedIn adds a hard ceiling: 5,000 campaigns and 15,000 creatives per account. |
| Campaign (holds the objective) | Campaign Group | A group holds no objective and no targeting. It is a budget-and-status envelope, nothing more. |
| Ad Set (budget, targeting, schedule) | Campaign | LinkedIn's Campaign carries the Ad Set's job and the objective — objectiveType lives here, one level lower than on Meta. |
| Ad | Creative | A Meta Ad wraps a creative. A LinkedIn Creative is the ad-level object, and it mostly just points elsewhere. |
| Lookalike Audience | — | LinkedIn's analogue is Audience Expansion (a boolean on the campaign) plus Matched Audiences. Different enough that the word will mislead you. |
Read the middle two rows as a shift by one level. Everything Meta puts on the Ad Set, LinkedIn puts on the Campaign — and then piles the objective on top of it. That is why the LinkedIn Campaign schema is enormous and the Campaign Group schema is tiny.
graph TD
subgraph M["Meta"]
MA["Ad Account"] --> MC["Campaign
objective"] --> MS["Ad Set
budget · targeting · schedule"] --> MD["Ad"]
end
subgraph L["LinkedIn"]
LA["Ad Account
currency · billing · reference"] --> LG["Campaign Group
budget ceiling · status"] --> LC["Campaign
objective · budget · targeting
schedule · bid · format"] --> LR["Creative
pointer + CTA"]
end
MC -.->|"same name,
different level"| LC
classDef m fill:#e0e7ff,stroke:#4338ca,color:#1e1b4b
classDef l fill:#dbeafe,stroke:#1d4ed8,color:#172554
classDef big fill:#fef3c7,stroke:#d97706,stroke-width:3px,color:#78350f
class MA,MC,MS,MD m
class LA,LG,LR l
class LC big
In the Creative schema, the campaign field is required and singular. A creative belongs to exactly one campaign, forever.
Correcting your calibration answer
You put targeting on the campaign — correct — but justified it with "the same creative can belong to multiple campaigns with different targets." That is false here. Reusing one image across three audiences means three separate creatives, each pointing at the same underlying post. The post is shared; the creative is not. If you model this as a many-to-many join table you will spend a week finding out why.
Cost of the mistake: you would silently break per-creative reporting, since analytics keys on sponsoredCreative. Two campaigns "sharing" a creative could never have separate numbers.
So the whole spine is one-to-many at every joint, and the joins are named after the parent:
type Creative = {
campaign: SponsoredCampaignUrn // required, singular, immutable in practice
account: SponsoredAccountUrn // read-only, derived from the campaign
content?: ContentUrn // → Lesson 02
}
type Campaign = {
account: SponsoredAccountUrn // immutable once set
campaignGroup: SponsoredCampaignGroupUrn // required since Oct 2020
}
Notice that the creative carries a redundant account — read-only, denormalised for query convenience. Your own schema should derive it, not store it, or you will eventually have two sources of truth that disagree.
A campaign group is created automatically when an ad account is created. It is easy to build an integration that never creates a group and quietly dumps everything into the default one. That works — until you hit the limit: 2,000 campaigns per non-default group, against 5,000 per account. Groups are unlimited in number, so create them deliberately.
Do these from memory. Scrolling up first feels productive and teaches you almost nothing — the retrieval effort is the learning.
Exercise 1 · recall
Type the namespace for each level. The urn:li: prefix is optional.
Careful: two of these are prefixes of two others. That is exactly why a substring check in your URN parser is a bug.
Exercise 2 · bug hunt
Three payloads. Click the one line in each that the API will reject. One of the three is correct — if you think so, click "No bug here".
A · POST /rest/adAccounts/{id}/adCreatives
{ "campaigns": ["urn:li:sponsoredCampaign:301", "urn:li:sponsoredCampaign:302"], "content": "urn:li:ugcPost:7099", "intendedStatus": "DRAFT", "name": "Q3 webinar — DACH"}
B · POST /rest/adAccounts/{id}/adCampaigns
{ "account": "urn:li:organization:2414183", "campaignGroup": "urn:li:sponsoredCampaignGroup:6088", "type": "SPONSORED_UPDATES", "costType": "CPC", "name": "CTO outreach"}
C · POST /rest/adAccounts/{id}/adCreatives
{ "campaign": "urn:li:sponsoredCampaign:301", "content": "urn:li:share:6844", "intendedStatus": "ACTIVE", "name": "Q3 webinar — variant b"}
Exercise 3 · execution
A new customer has just completed OAuth. They want one image ad live. Click the steps in the order you must perform them — each one needs an ID that a previous one returned.
Exercise 4 · interleaved translation
Give the LinkedIn level that does each job. Answer with a level name from the spine — or none if no single level owns it.
Three of the six answers are the same word. That is the point — LinkedIn's Campaign is where nearly all configuration lives.
Three things, in descending order of value:
Next, on the outer fringe
Lesson 02 — Acting entities and the content graph: who can be an advertiser (reference), who the ad is for (associatedEntity), who appears to have said it (contentAuthor), and the six-way union that field content accepts. It answers the second half of your original question directly.
Every claim here traces to Create and Manage Creatives, Create and Manage Campaigns, and Create and Manage Ad Accounts, version li-lms-2026-08.