# Run ads as a partner (/docs/api/guides/run-ads-as-partner)



This guide walks the full partner-side ads write path. Every write is gated by the customer's [bucket-mode authority](/docs/api/concepts/ads-write-model) — partner keys cannot escalate past customer policy. By the end you'll have a working campaign created on Meta or TikTok, with Layers' optimizer running against it.

Apple Search Ads now supports campaign create from the partner API — see the [LOCK 10 investigation](/docs/internal/apple-campaign-create-investigation) for the lift. {/* AGENT-18-OUTPUT-PENDING: confirm Apple campaign-create endpoint shape once Agent 18 ships the new partner route mirroring the Meta/TikTok variants. */}

## Prerequisites [#prerequisites]

* A project on a Layers org you have a partner key for.
* A partner key with the relevant `ads:write:*` sub-scopes ([scope vocabulary](/docs/api/concepts/api-keys#ads-sub-scopes)). For this guide: `ads:write:connect`, `ads:write:campaigns`, `ads:write:budgets`, `ads:write:lifecycle`, `ads:write:optimizer_trigger`, `ads:write:pending`, `ads:read`.
* The customer has consented to partner writes on the project layer (one-time toggle in the customer dashboard).

## 1. Connect the customer's ad account [#1-connect-the-customers-ad-account]

OAuth init returns a URL to redirect the customer to. They authorize Meta / TikTok / Apple, then return to your app via `returnUrl`.

```bash
curl -X POST https://api.layers.com/v1/projects/$PROJECT_ID/ads/ad-accounts/oauth-url \
  -H "Authorization: Bearer $LAYERS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "platform": "meta_ads",
    "returnUrl": "https://partner.example.com/customers/acme/ads/connected"
  }'
```

The customer signs in to Meta, picks the ad account they want to use, and redirects to your `returnUrl` with `?status=connected&accountId=…`.

When the OAuth flow completes, Layers stores the BYO ad account on the layer. List with [`GET …/ad-accounts`](/docs/api/reference/ads/list-ad-accounts).

## 2. Import existing campaigns OR create a new one [#2-import-existing-campaigns-or-create-a-new-one]

### Existing campaigns [#existing-campaigns]

If the customer is already running campaigns on Meta / TikTok and just wants Layers to manage them, **no import is needed** — `GET …/campaigns` lists every campaign on the connected ad account, including pre-Layers ones. Subsequent writes (`PATCH …/campaigns/:id/budget`, `…/pause`, etc.) flow through the bucket-mode authority gate against whatever per-campaign authority defaults you configure.

To attach Layers' optimizer to an existing campaign, set per-campaign authority:

```bash
curl -X PATCH https://api.layers.com/v1/projects/$PROJECT_ID/ads/meta/campaigns/$CAMPAIGN_ID/authority \
  -H "Authorization: Bearer $LAYERS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "authority": {
      "creative": "auto",
      "tactical": "draft",
      "structural": "off"
    }
  }'
```

### Create a new campaign (simplified surface) [#create-a-new-campaign-simplified-surface]

The headline endpoint takes four required fields. Layers picks every other tactical detail per platform best practice; sophisticated partners can override via the granular CRUD endpoints.

```bash
curl -X POST https://api.layers.com/v1/projects/$PROJECT_ID/ads/meta/campaigns \
  -H "Authorization: Bearer $LAYERS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "adAccountId": "aa_01HXF1...",
    "name": "Q3 prospecting",
    "outcome": "conversions",
    "dailyBudgetCents": 5000,
    "campaignManagementMode": "draft",
    "creativeMode": "autopilot"
  }'
```

`5000` cents = $50.00 daily budget on every platform. Cents universally — see [LOCK 2](/docs/api/concepts/ads-write-model#currency).

The 202 response carries `{ jobId, campaignId, verdictId }`. Poll `jobId` until terminal; `campaignId` is the Layers-side id (the platform-side id is exposed via `GET …/campaigns/:id` once the workflow lands the real platform mutation).

`verdictId` is the audit-row UUID — quote it to Layers support if anything goes sideways.

## 3. Configure the authority block (if needed) [#3-configure-the-authority-block-if-needed]

The simplified create endpoint set:

* `campaignManagementMode: "draft"` → `tactical: "draft"`, `structural: "draft"`
* `creativeMode: "autopilot"` → `creative: "auto"`

If you want a different combination, PATCH the new campaign's authority:

```bash
curl -X PATCH https://api.layers.com/v1/projects/$PROJECT_ID/ads/meta/campaigns/$CAMPAIGN_ID/authority \
  -H "Authorization: Bearer $LAYERS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "authority": { "creative": "auto", "tactical": "draft", "structural": "off" } }'
```

That PATCH is itself gated — the customer's layer-defaults policy can refuse it.

## 4. Trigger the optimizer [#4-trigger-the-optimizer]

The optimizer evaluates 30 days of paid performance and proposes changes (push/replace creatives, budget tweaks, kill underperformers). Trigger it on demand:

```bash
curl -X POST https://api.layers.com/v1/projects/$PROJECT_ID/ads/optimizer/run \
  -H "Authorization: Bearer $LAYERS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "layerId": "lyr_8b3c1d2e-4f5a-46b7-9c8d-0e1f2a3b4c5d",
    "platform": "meta_ads",
    "dryRun": false
  }'
```

The 202 returns `{ jobId, runId }`. The full run takes 5–15 minutes. Subscribe to `ads.optimizer.run.completed` for completion (with humanized change list) or poll `GET …/optimizer/runs/:runId`.

## 5. Observe the pending queue [#5-observe-the-pending-queue]

Authority axes set to `draft` (which the simplified create defaulted `tactical` to) cause the optimizer's output to **queue** as pending actions instead of dispatching to the platform.

```bash
curl "https://api.layers.com/v1/projects/$PROJECT_ID/ads/meta/pending?status=pending" \
  -H "Authorization: Bearer $LAYERS_API_KEY"
```

Each row carries:

* `actionType` — `pause_ad`, `update_budget`, `push_creative`, etc.
* `entityName` — the campaign / adset / ad name.
* `currentValue` / `newValue` — what would change.
* `rationale` — LLM-summarized explanation.
* `proposedAction.detail` — full platform-specific payload (creative urls, targeting deltas).

## 6. Approve or reject [#6-approve-or-reject]

```bash
# Approve
curl -X POST https://api.layers.com/v1/projects/$PROJECT_ID/ads/meta/pending/$PENDING_ID/approve \
  -H "Authorization: Bearer $LAYERS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "comment": "Looks good — spend pacing is on target." }'

# Reject
curl -X POST https://api.layers.com/v1/projects/$PROJECT_ID/ads/meta/pending/$PENDING_ID/reject \
  -H "Authorization: Bearer $LAYERS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Customer wants to leave this creative running through Friday." }'
```

Approved actions dispatch on the optimizer's next 6h tick (or earlier if you trigger another run). The `dispatchedAt` timestamp on the pending-action row populates when the action lands on the platform, and the `approval.dispatched` webhook fires.

## What's already gated [#whats-already-gated]

Even with all the right scopes, certain things are by-design unreachable:

* **Kill switch** — customer-only writable. Partners observe via the audit log + `ads.write.denied` webhook.
* **Bucket-mode defaults** — partners can PATCH them with `ads:write:policy`, but the customer's layer is the source of truth. (See [LOCK 1](/docs/api/concepts/ads-write-model) — parity flavor.)

## What's never partner-writable [#whats-never-partner-writable]

* **Layer-level kill switch.** No `ads:write:kill_switch` scope exists.
* **Customer billing / payment methods.** Partners do not transact platform spend; the customer's connected ad account is the source of money.

## See also [#see-also]

* [Ads write model concept page](/docs/api/concepts/ads-write-model)
* [API keys — ads sub-scopes](/docs/api/concepts/api-keys#ads-sub-scopes)
* [Ads reference index](/docs/api/reference/ads)
* [Audit log](/docs/api/reference/audit-log/list)
* [Webhooks](/docs/api/operational/webhooks)
