Lesson 06 · outer fringe
Intent and effect are different fields. Five of them, spread unevenly across four entities, and the one you most want — "is this ad actually running, and if not why" — is a read-only array that is absent when the answer is yes.
≈15 min · pinned to li-lms-2026-08
Every status-shaped field in this API is on one side of a line:
Intent · you write it
What the advertiser asked for. Mutable. Yours to set and to store.
status
intendedStatus
Effect · the service writes it
What is actually happening. Read-only. Never store it as truth — cache it with a timestamp or not at all.
servingStatuses
servingHoldReasons
isServing
The docs say it plainly for campaigns: servingStatuses is an array "that determine whether or not a campaign can be served; unlike status, which is user-managed, the values are controlled by the service."
If you take one thing: a PATCH that sets status: ACTIVE and returns 200 has told you nothing about whether an ad will run. Intent succeeded. Effect is a separate question, answered by a separate read, at a later time.
This is the part that cannot be guessed, and the part where Meta's model actively misleads: there, every level has an effective_status. Here the fields are inconsistent by level.
| Entity | status | intendedStatus | servingStatuses | servingHoldReasons | isServing |
|---|---|---|---|---|---|
| Ad Account | ✓ | — | — | — | — |
| Campaign Group | ✓ | — | ✓ | — | — |
| Campaign | ✓ | — | ✓ | — | — |
| Creative | absent | ✓ | — | ✓ | ✓ |
A creative has no status field
It has intendedStatus instead — and the rename is not cosmetic, it is a confession. The docs: "the creative intended status is set independently from parent entity status, but parent entity status overrides creative intended status in effect. For example, parent entity status may be PAUSED while creative status is ACTIVE, in which case the creative's effective status is PAUSED."
So LinkedIn renamed the field to stop you reading it as effective state. Campaigns kept the ambiguous name status and bolted servingStatuses alongside; creatives got the honest name and servingHoldReasons + isServing. Same idea, two eras, two spellings. Do not build an abstraction that pretends the four levels are uniform — they aren't, and hiding it costs you the diagnosis.
Here is the payoff, and the thing that makes the vocabulary genuinely useful rather than merely verbose. A creative's servingHoldReasons contains not just its own problems but its ancestors'. The array is a flattened diagnosis of the whole chain above it.
graph TD A["Ad Account"] -->|"ACCOUNT_SERVING_HOLD
ACCOUNT_TOTAL_BUDGET_HOLD
ACCOUNT_END_DATE_HOLD"| G G["Campaign Group"] -->|"CAMPAIGN_GROUP_STATUS_HOLD
CAMPAIGN_GROUP_START_DATE_HOLD
CAMPAIGN_GROUP_END_DATE_HOLD
CAMPAIGN_GROUP_TOTAL_BUDGET_HOLD"| C C["Campaign"] -->|"CAMPAIGN_STOPPED
CAMPAIGN_START_DATE_HOLD
CAMPAIGN_END_DATE_HOLD
CAMPAIGN_TOTAL_BUDGET_HOLD
CAMPAIGN_AUDIENCE_COUNT_HOLD"| R R["Creative
servingHoldReasons"] R --- O["own reasons:
STOPPED · UNDER_REVIEW · REJECTED
PROCESSING · PROCESSING_FAILED
FORM_HOLD · JOB_POSTING_ON_HOLD
JOB_POSTING_INVALID
REFERRED_CONTENT_QUALITY_HOLD"] classDef lvl fill:#dbeafe,stroke:#2563eb,color:#1e3a5f classDef own fill:#fef3c7,stroke:#d97706,color:#78350f class A,G,C,R lvl class O own
Read the prefixes as addresses. ACCOUNT_* means go fix the account; CAMPAIGN_GROUP_* means the layer you were tempted to skip in Lesson 01 is now the reason nothing runs. The unprefixed ones — STOPPED, UNDER_REVIEW, REJECTED — are the creative's own.
Two traps in the shape of the field
Absent means healthy. "In the case a creative is being served, this field will be null and not present in the response." So holdReasons.length === 0 is the wrong check — the key isn't there. An optional-array field where absence is the success case will produce a TypeError in exactly the situation you are least likely to test.
STOPPED is overloaded. It appears in the creative's list meaning "stopped by the advertiser" — that is, this creative was paused. The campaign's own equivalent is CAMPAIGN_STOPPED. But servingStatuses on a campaign also has a bare STOPPED, meaning the campaign is not servable. Same token, two entities, two meanings. Never compare hold-reason strings across levels.
A creative can be intendedStatus: ACTIVE, its whole ancestor chain healthy, and still not serve — because a human or a model has not approved it yet. That lives in review, which is read-only and, importantly, absent while the creative is in DRAFT. Review starts when the creative is activated.
| review.status | Means | Corresponding hold reason |
|---|---|---|
| PENDING | Awaiting review, not serving. | UNDER_REVIEW |
| APPROVED | Cleared — includes pre-approved and model-auto-approved. | — |
| REJECTED | Refused. rejectionReasons[] tells you why, from a list of ~70 policy codes. | REJECTED |
| NEEDS_REVIEW | The model declined to decide. Awaiting a human. | UNDER_REVIEW |
NEEDS_REVIEW deserves a note, because its name is a lie about its cause. It does not mean "someone requested a review." The docs: the creative "has been rejected by content model or policy checker or returned by fallback case that auto approval didn't make any decision." It is the uncertain bucket. For your product that means it is not a failure to report to the customer and not a success either — it is a wait, of unbounded length, and you should render it differently from PENDING.
The rejectionReasons list is worth a skim once, not memorising. Its useful property: many codes are about the landing page, not the ad — NONFUNCTIONAL_SITE, MISSING_PRIVACY_POLICY, BACK_BUTTON_NOT_WORKING, INCONSISTENT_DISPLAY_AND_LANDING_PAGE_URLS. A rejection is not always something your customer can fix by editing copy, and your error surface should not imply that it is.
One state transition is forbidden
"You can't pause an ad creative in review. This endpoint returns a 400 error if you attempt to change the status of an in-review ad creative to paused." So ACTIVE → PAUSED is not always available. If your product exposes a pause toggle, it has a window where the toggle must be disabled — and the only way to know is to have read review.status first.
Exercise 1 · execution
A customer says "my ad isn't showing." You have their account. Click the steps in the order that finds the answer with the fewest wasted calls.
Exercise 2 · recognition
You read a creative's servingHoldReasons. For each value, which level do you have to go and fix?
Exercise 3 · bug hunt
TypeScript that reasons about serving state. Click the offending line, or say there is no bug. One of the three is clean.
A · "is this ad running?"
function isRunning(c: Creative): boolean { if (c.intendedStatus !== 'ACTIVE') return false if (c.review?.status !== 'APPROVED') return false return c.servingHoldReasons.length === 0}
B · "did the pause take effect?"
async function pause(id: CreativeUrn) { const before = await getCreative(id) if (before.review?.status === 'PENDING') throw new InReviewError() await patchCreative(id, { intendedStatus: 'PAUSED' }) await db.creatives.update(id, { paused: true })}
C · "surface the blocker to the customer"
const OWN = new Set(['STOPPED', 'UNDER_REVIEW', 'REJECTED', 'PROCESSING', 'PROCESSING_FAILED', 'FORM_HOLD', 'REFERRED_CONTENT_QUALITY_HOLD', 'JOB_POSTING_ON_HOLD', 'JOB_POSTING_INVALID']) function blockers(c: Creative) { const reasons = c.servingHoldReasons ?? [] const mine = reasons.filter(r => OWN.has(r)) return mine.length ? { level: 'creative', reasons: mine } : { level: 'ancestor', reasons }}
Exercise 4 · recall
Exercise 5 · execution
Here is a real-shaped response. Write down, in order of what you would tell the customer to do: how many distinct problems are there, at which levels, and which one must be fixed first?
{
"id": "urn:li:sponsoredCreative:120560935",
"campaign": "urn:li:sponsoredCampaign:360035215",
"intendedStatus": "ACTIVE",
"isServing": false,
"review": { "status": "PENDING" },
"servingHoldReasons": [
"UNDER_REVIEW",
"CAMPAIGN_STOPPED",
"ACCOUNT_SERVING_HOLD",
"CAMPAIGN_GROUP_STATUS_HOLD"
]
}
Now unlocked
You can configure a campaign and explain why it isn't running. The remaining gap before you can design your multi-tenant schema is authorisation: who is allowed to do any of this, on whose behalf. Lesson 07 — Who Is Allowed — covers adAccountUsers, the five roles, and why a valid token with the right scope still returns 403.
Sources: Create and Manage Creatives (creative schema, CreativeReview, servingHoldReasons, rejection reasons), Create and Manage Campaigns (status, servingStatuses). Version li-lms-2026-08.