Ads write model
How partner ad writes are gated — bucket-mode authority, customer controls, partner inheritance, kill-switch contract, and what's never partner-writable.
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
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 reasonauthority_axis_off.draft— write attempts queue as pending optimizer actions. The customer (or a partner key withads: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 is the source of truth for these axes. Partners can inspect the current authority via GET …/{platform}/campaigns/:cid/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
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. 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.
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:
- Partner POSTs
…/campaigns/:id/budgetwithdailyBudgetCents: 7500. - Gate reads campaign's
tacticalaxis. - If
auto→ dispatch (platform sees the budget change immediately). Audit row written,verdictIdreturned in response. - If
draft→ queue asoptimizer_pending_actionsrow. Response is202with the pending-action id; partner can approve viaPOST …/pending/:id/approveif scopeads:write:pendingis held, or wait for customer approval. - If
off→403with reasonauthority_axis_off. No platform-side effect.
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
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 carries
kill_switch.trippedandkill_switch.releasedevents. - The
ads.write.deniedwebhook fires for every blocked write while the kill switch is active. Partners should pause their own write loops until they seekill_switch.released. - The
GET …/{platform}/defaultsresponse surfaceskill_switch_active: bool.
What's 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_reasontext 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; the customer completes the platform-side ownership grant.
Currency
All money fields on the partner contract — budgets, bids, caps — are integers in cents (LOCK 2). 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
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
- Run ads as partner — end-to-end guide
- API keys — ads sub-scope table
- Audit log
- Webhooks
- Apple campaign-create investigation — why the previous unconditional Apple deny is being lifted