Layers
Partner APIConcepts

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.

View as Markdown

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:

AxisMeaningModes
creativePush / replace / prune ad creatives.off | draft | auto
tacticalBudget, bidding, schedule, targeting.off | draft | auto
structuralCreate / 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 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:

FieldTypeNotes
dailyBudgetCents or lifetimeBudgetCentsintegerRequired. Cents universally per LOCK 2. Example: 5000 = $50.00 daily.
outcomeenumPlatform-normalized outcome (conversions, traffic, app_installs, etc.). Layers maps to platform-specific objective enums.
campaignManagementModeenummanual | draft | autopilot — sets the new campaign's tactical + structural authority.
creativeModeenummanual | draft | autopilot — sets the new campaign's creative authority.

Mode → axis mapping:

  • manualoff (no automation; customer drives every change)
  • draftdraft (queue everything for approval)
  • autopilotauto (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:

  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 off403 with reason authority_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.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

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; 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

On this page