# Ads write model (/docs/api/concepts/ads-write-model)



The Layers Partner API exposes the same ads write surface the Layers customer-facing app exposes — partner authentication is swapped for the customer JWT, and the customer's per-campaign and per-layer authority remains the only behavioral gate. This page describes the gate so partners can predict which writes will allow, queue, or deny.

**BYO-only.** The customer's connected ad accounts are the source of money and consent. Partners do not transact platform spend; there is no Layers wallet that funds ad spend, and no aggregated billing.

## Bucket-mode authority [#bucket-mode-authority]

Every campaign in Layers carries an **authority block** with three axes:

| Axis         | Meaning                                                            | Modes                      |
| ------------ | ------------------------------------------------------------------ | -------------------------- |
| `creative`   | Push / replace / prune ad creatives.                               | `off` \| `draft` \| `auto` |
| `tactical`   | Budget, bidding, schedule, targeting.                              | `off` \| `draft` \| `auto` |
| `structural` | Create / pause / resume / archive / delete campaigns, adsets, ads. | `off` \| `draft` \| `auto` |

For each axis:

* **`off`** — write attempts are **denied** with reason `authority_axis_off`.
* **`draft`** — write attempts **queue** as pending optimizer actions. The customer (or a partner key with `ads:write:pending`) must approve before they dispatch to the platform.
* **`auto`** — write attempts **dispatch immediately** through the platform proxy after the gate's safety checks pass.

The customer's [bucket-mode dashboard](https://app.layers.com) is the source of truth for these axes. Partners can inspect the current authority via [`GET …/{platform}/campaigns/:cid/authority`](/docs/api/reference/ads/patch-campaign-authority) and (with `ads:write:policy`) propose authority changes via PATCH — those PATCHes also flow through the gate, so a partner cannot escalate themselves past customer policy.

## Simplified create surface [#simplified-create-surface]

For the headline "Create Campaigns on Meta / TikTok / Apple" button, the partner endpoint requires four fields:

| Field                                         | Type    | Notes                                                                                                                           |
| --------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `dailyBudgetCents` *or* `lifetimeBudgetCents` | integer | Required. **Cents universally** per [LOCK 2](#currency). Example: `5000` = $50.00 daily.                                        |
| `outcome`                                     | enum    | Platform-normalized outcome (`conversions`, `traffic`, `app_installs`, etc.). Layers maps to platform-specific objective enums. |
| `campaignManagementMode`                      | enum    | `manual` \| `draft` \| `autopilot` — sets the new campaign's `tactical` + `structural` authority.                               |
| `creativeMode`                                | enum    | `manual` \| `draft` \| `autopilot` — sets the new campaign's `creative` authority.                                              |

Mode → axis mapping:

* `manual` → `off` (no automation; customer drives every change)
* `draft` → `draft` (queue everything for approval)
* `autopilot` → `auto` (dispatch automatically)

Optional fields (`bidStrategy`, `targeting`, `schedule`, etc.) are defaulted by Layers per platform best practice. Sophisticated partners can use the granular CRUD endpoints (per-budget, per-targeting, etc.) for explicit overrides — see the [ads reference](/docs/api/reference/ads).

## Authority inheritance for partner-issued writes [#authority-inheritance-for-partner-issued-writes]

A partner-issued write is gated identically to a Layers-customer-issued write — the gate consults the campaign's authority block and the layer-level defaults. The only difference is the audit row's `actor_type`, which is `"partner"` (with `actor_organization_id` and `actor_api_key_id` populated for partner-attribution queries).

Concretely:

1. Partner POSTs `…/campaigns/:id/budget` with `dailyBudgetCents: 7500`.
2. Gate reads campaign's `tactical` axis.
3. If `auto` → dispatch (platform sees the budget change immediately). Audit row written, `verdictId` returned in response.
4. If `draft` → queue as `optimizer_pending_actions` row. Response is `202` with the pending-action id; partner can approve via `POST …/pending/:id/approve` if scope `ads:write:pending` is held, or wait for customer approval.
5. If `off` → `403` with reason `authority_axis_off`. No platform-side effect.

## Per-campaign vs defaults [#per-campaign-vs-defaults]

Each platform layer (`layer_meta_ads`, `layer_tiktok_ads`, `layer_apple_ads`) carries a **defaults block** — the fallback authority for campaigns that don't have an explicit per-campaign authority row. The defaults block is editable via `PATCH …/{platform}/defaults` (scope `ads:write:policy`).

Per-campaign authority always overrides defaults. New campaigns inherit defaults at create time; subsequent PATCHes can pin per-campaign authority that diverges.

## Kill switch [#kill-switch]

Each platform layer has a **layer-level kill switch** that immediately denies every write to that layer's ads, regardless of axis state. The kill switch exists for emergency stop scenarios (creative gone wrong, customer billing issue, platform-side incident).

**Partner contract:** the kill switch is **customer-only writable**. There is no `ads:write:kill_switch` scope. Partners cannot trip or release the kill switch.

Partners observe kill-switch state passively:

* The [audit log](/docs/api/reference/audit-log/list) carries `kill_switch.tripped` and `kill_switch.released` events.
* The `ads.write.denied` webhook fires for every blocked write while the kill switch is active. Partners should pause their own write loops until they see `kill_switch.released`.
* The `GET …/{platform}/defaults` response surfaces `kill_switch_active: bool`.

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

Even with the most permissive partner key, certain things are never reachable from the partner API by design:

* **Kill switch** (above). Customer-only.
* **The Layers-internal optimizer-output review process** (`flagged_reason` text on creatives is observable but the review queue is internal).
* **Platform-side ad-account ownership** (Meta Business Manager grants, TikTok BC permissions, Apple Search Ads org membership). Partner triggers OAuth via [`POST …/oauth-url`](/docs/api/reference/ads/ad-accounts-oauth-url); the customer completes the platform-side ownership grant.

## Currency [#currency]

All money fields on the partner contract — budgets, bids, caps — are integers in **cents** ([LOCK 2](#currency)). Layers translates at the proxy boundary:

* **Meta** uses cents — pass-through.
* **TikTok** uses dollars — Layers divides by 100.
* **Apple** uses `{amount: "<dollars-decimal>", currency: ISO}` — Layers converts. Default currency is the layer's currency, fallback `"USD"`.

This insulates partners from platform-API quirks. Sample: `dailyBudgetCents: 5000` is "$50.00 daily budget" on every platform.

## Verdict id [#verdict-id]

Every gate decision (`allow`, `queue_for_approval`, `deny`) is logged as a `layer_ads_audit` row. The row's `id` is surfaced on the partner write response as `verdictId` (UUID). Quote it to Layers support to find the exact gate decision that produced your response.

`verdictId` does **not** enable replay (replay still requires `Idempotency-Key`, and ads writes do not require Idempotency-Key per the read–decide–write pattern).

## See also [#see-also]

* [Run ads as partner](/docs/api/guides/run-ads-as-partner) — end-to-end guide
* [API keys — ads sub-scope table](/docs/api/concepts/api-keys#ads-sub-scopes)
* [Audit log](/docs/api/reference/audit-log/list)
* [Webhooks](/docs/api/operational/webhooks)
* [Apple campaign-create investigation](/docs/internal/apple-campaign-create-investigation) — why the previous unconditional Apple deny is being lifted
