> ## Documentation Index
> Fetch the complete documentation index at: https://docs.appdna.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# LinkedIn Ads

> Connect a LinkedIn Ads sponsored account to manage campaign groups, campaigns, creatives, asset uploads, and ad analytics, and run multi-campaign creative experiments from the dashboard.

## Overview

Connecting LinkedIn Ads lets the dashboard:

* Create, update, pause, resume, and archive **campaign groups** (the budget container) and **campaigns** (targeting + delivery units).
* Create **creatives** bound to a campaign and run a 2-step asset upload flow for **images, videos, and documents**.
* Pull spend, impressions, clicks, conversions, and cost-in-USD analytics.
* Run creative experiments by rotating multiple campaigns under one campaign group.

The integration uses the **LinkedIn Marketing API v202402** with **OAuth 2.0** authentication and refresh tokens.

## Prerequisites

Before connecting, make sure you have:

1. A LinkedIn Business account with at least one sponsored ad account.
2. A user role on the sponsored account that allows campaign management.
3. **Marketing Developer Platform** access. LinkedIn requires partner review of the connecting application before live writes are allowed. Live write access is enabled once LinkedIn partner approval is granted for your account.
4. An OAuth 2.0 application configured for the dashboard's redirect URI, with OAuth credentials configured by your administrator.

## OAuth scopes

The connection requests the following scopes:

```
r_ads
r_ads_reporting
rw_ads
```

`r_ads` and `r_ads_reporting` cover list and analytics calls; `rw_ads` covers all create / update operations on campaign groups, campaigns, and creatives.

## Connect

1. Open the **Paid UA → Integrations** page in the dashboard.
2. Click **Connect** on the LinkedIn Ads tile.
3. Sign in with the LinkedIn user that has access to your sponsored account.
4. Approve the requested scopes.
5. After redirect, the dashboard exchanges the auth code for tokens and probes `/adAccounts?q=search` to confirm access and capture the connected sponsored accounts.

Access tokens are rotated automatically before expiry using the refresh token.

## Naming map

LinkedIn's domain model uses different terms from the dashboard's cross-platform DTO. Internally the integration maps:

| Dashboard DTO | LinkedIn resource  | Notes                                             |
| ------------- | ------------------ | ------------------------------------------------- |
| `campaign`    | `campaignGroup`    | The budget container                              |
| `adSet`       | `campaign`         | The targeting + delivery unit                     |
| `ad`          | `creative`         | Bound to a `campaign` by URN                      |
| `adAccount`   | `sponsoredAccount` | Referenced by URN: `urn:li:sponsoredAccount:{id}` |

## What happens behind the scenes

| Operation                                        | API endpoint                                                         |
| ------------------------------------------------ | -------------------------------------------------------------------- |
| List sponsored accounts                          | `GET /adAccounts?q=search`                                           |
| List / create campaign groups                    | `GET / POST /adAccounts/{id}/adCampaignGroups`                       |
| Update / pause / resume / archive campaign group | `POST /adAccounts/{id}/adCampaignGroups/{id}` (Rest.li `patch.$set`) |
| List / create campaigns (= ad sets)              | `GET / POST /adAccounts/{id}/adCampaigns`                            |
| Update / pause / resume / archive campaign       | `POST /adAccounts/{id}/adCampaigns/{id}` (Rest.li `patch.$set`)      |
| Create / update creative                         | `POST /creatives[/{id}]`                                             |
| Register asset upload                            | `POST /assets?action=registerUpload`                                 |
| Pull analytics                                   | `GET /adAnalytics?q=analytics&pivot=CAMPAIGN&...`                    |

Update operations send a Rest.li **partial update** envelope:

```json theme={null}
{ "patch": { "$set": { "name": "Renamed", "status": "PAUSED" } } }
```

Pause / resume / archive simply flip the `status` field (`ACTIVE`, `PAUSED`, `ARCHIVED`). LinkedIn does **not** hard-delete campaign groups or campaigns — the cancel verb is `ARCHIVED`.

Every REST request carries the required versioning headers:

```
LinkedIn-Version: 202402
X-Restli-Protocol-Version: 2.0.0
```

Every write request additionally carries an `Idempotency-Key` HTTP header so retries do not produce duplicate resources, even if the connection drops mid-request.

## Creatives & asset uploads

LinkedIn's asset upload is a **two-step flow**:

1. The dashboard calls `POST /assets?action=registerUpload` with the recipe URN (`urn:li:digitalmediaRecipe:feedshare-image` or `urn:li:digitalmediaRecipe:feedshare-video`) and the owner URN of the entity that will own the asset (typically the connected organization).
2. The dashboard receives an `uploadUrl` plus an asset URN, and the bytes of the image / video are PUT directly to the returned URL.

Once the bytes land, the asset URN is referenced from a creative via `POST /creatives` with the asset URN bound into the creative payload.

## Budget conversion

LinkedIn budgets are stored as `{ amount: "<whole-currency>", currencyCode: "USD" }`. The dashboard sends USD amounts in its DTOs and serialises them into the LinkedIn currency-amount shape on the request.

`dailyBudget` and `totalBudget` map to the DTO's daily and lifetime budget types respectively.

## Rate limiting

Requests are rate limited. LinkedIn's limits are partner-specific — **100 requests per day** for unreviewed apps and up to several thousand per second for approved partners. If the platform reports HTTP 429, the request is automatically retried with exponential backoff.

If the rate limiter is unavailable, it fails closed and the request is reported as a quota error to the platform's automation — preventing accidental over-quota fan-out under outage.

## Reporting

Analytics are pulled hourly (or on demand) from `/adAnalytics?q=analytics`. The dashboard requests the `CAMPAIGN_GROUP` (or `CAMPAIGN`) pivot with `DAILY` granularity by default. Default fields include `externalWebsiteConversions`, `impressions`, `clicks`, and `costInUsd`. Spend is reported in USD natively for USD-denominated accounts; non-USD ad accounts have local-currency spend converted to USD on ingestion.

## Experiments (multi-campaign rotation)

LinkedIn does **not** expose a native split-test API. The dashboard maps experiment variants onto **multiple campaigns** under a single campaign group, with each variant carrying its own targeting and creative. The control variant ships **active**; treatments ship **paused** until the experiment runner toggles them. Per-variant performance is read back from the analytics endpoint scoped to the parent campaign group.

## Disconnect

Click **Disconnect** on the integration tile. The OAuth refresh token is removed from the dashboard. To fully revoke access, also remove the third-party app authorisation from your LinkedIn account's connected-apps list.

## Troubleshooting

* **"LinkedIn Ads /adAccounts returned 401"** — the OAuth token has been revoked or has expired without a valid refresh token. Re-connect the integration.
* **"LinkedIn Ads create\_campaign\_group: 429 — ..."** — your app's rate budget was breached. The dashboard backs off automatically; if the error persists, request a higher Marketing Developer Platform throughput tier.
* **Writes are not reaching LinkedIn** — live write access is enabled once LinkedIn partner approval is granted for your account. Contact your administrator if writes are not taking effect after approval.
* **"webhooks not supported on linkedin\_ads"** — the Marketing API does not expose webhooks. State changes (campaign disapproved, daily cap hit) are detected via the hourly metrics-sync job.
* **`registerUpload` returns 400 with `owner` empty** — the upload owner URN was not derivable from the supplied DTO. Pass the organisation URN (e.g. `urn:li:organization:1234`) in the DTO's `asset_id` field, or call the `register_upload` adapter operation directly with the full body.
* **Update returns 405 Method Not Allowed** — LinkedIn's update verb is **POST** with a `patch.$set` envelope, not PATCH or PUT. The integration handles this automatically; if you see this error, please report it.
