Docs
ApiOperational

Changelog

Dated, reverse-chronological. One entry per shipped change. No marketing.

View as Markdown

Every shipped change lands here on the day it ships. Grouped by the standard Keep a Changelog categories: Added / Changed / Deprecated / Removed / Fixed. Dates are UTC.

Subscribe by RSS or watch the repo - your call.

Planned

Scheduled for the next release. Shape may change before launch.

Planned - Added

  • Webhooks: signed POST delivery, HMAC-SHA256 signatures, retry policy, subscription registration. See Webhooks for the committed shape.
  • Ads CRUD on Meta, TikTok, Apple (POST/PATCH /v1/projects/:id/ads/{campaigns,adsets,ads}).
  • Approval v2: trust_threshold + current_trust_score for auto-transition to continuous approval.
  • (none — see Changed below for engagement template v2)
  • SSE streaming on GET /v1/jobs/:jobId/events (optional - polling keeps working).
  • Recommendations taxonomy: new_hook, cadence, audience_gap, ads_budget.
  • Per-project credit CSV export for partner rebilling.

2026-09-02 — Creative eligibility matches what Layers will actually promote; adsContentId round-trips

Changed

  • GET /v1/projects/:id/ads-content, PATCH /v1/projects/:id/ads-content/:id and GET /v1/projects/:id/top-performers now answer eligibility with the same predicate Layers applies when it builds the ad. Previously the partner surface judged only the manual override, the safety verdict and the moderator flag, so a creative could be reported eligible and then refused at build time. Three refusals are now expressible, as additive eligibility.reason values: spark_authorization_not_live, spark_window_unknown and source_unavailable. Every reason value returned before this change still means what it meant, and ?eligible=true / the default top-performers gate follow the same rule. The deprecated creative score is still never consulted.
  • GET /v1/projects/:id/ads/ads and the paid rows of GET /v1/projects/:id/top-performers now return adsContentId in the prefixed adc_<UUID> form both response schemas already declared. It was previously the bare UUID. If you store adsContentId, store the new string — the filter below accepts the prefixed form, which is what every endpoint now emits.
  • GET /v1/projects/:id/ads-content and PATCH /v1/projects/:id/ads-content/:id now return sourceId in the prefixed cnt_<UUID> form for content-container creatives, and null for every other origin. It previously carried a bare UUID — for a container, and for other origins an internal row key the field's own schema (ContentContainerId) never accepted. A published post continues to report its identity on platformPostId.
  • GET /v1/projects/:id/ads/ads: name is a human-readable label again. It duplicated the advertising platform's own ad identifier, which belongs in externalId and still appears there. name now carries the parent ad set's (Meta) or ad group's (TikTok) stored name, falling back to "Meta ad" / "TikTok ad".
  • GET /v1/projects/:id/top-performers: the organic ranking now pages past its candidate window when the eligibility gate refuses rows, so a refused post no longer costs you a slot. Previously the gate ran inside a fixed first 1,000 candidates, and an account whose top posts were all ineligible could return fewer rows than limit — or none — while promotable posts sat just past the cut.

Fixed

  • GET /v1/projects/:id/ads/ads?adsContentId=… returned an empty list for every id it accepted. The filter requires the prefixed adc_<UUID> form (a bare UUID is a 422), but the handler compared that prefixed string against the bare-UUID column, so nothing ever matched. The documented "where is this creative running?" query now works end to end.

2026-09-02 — Platform-post IDs are prefixed; paid top-performer titles carry no provider IDs

Changed

  • GET /v1/projects/:id/ads-content, PATCH /v1/projects/:id/ads-content/:id, and the organic path of GET /v1/projects/:id/top-performers now return platformPostId in the opaque prefixed form pp_<UUID>. It was previously the bare platform_posts UUID — an internal row key on the partner wire. pp_ is the shape top-performers already documented and the one GET /v1/projects/:id/metrics?scope=platform_post&id= already accepts, so the value round-trips unchanged. If you store platformPostId, store the new string — per the tolerance rule the bare UUID you already hold keeps resolving, so nothing breaks on the way in.
  • pp_<UUID> is now listed in the ID formats catalog.

Fixed

  • GET /v1/projects/:id/top-performers: the title on a paid row was "<platform> ad <provider ad id>" — the advertising platform's own ad identifier, in the field documented as a human-readable title. It now carries the campaign name as stored in Layers, falling back to "Meta ads campaign" / "TikTok ads campaign" when a campaign has no stored name. Provider ad IDs no longer appear anywhere in the response.

2026-08-31 — Leased accounts removed from the Partner API

Removed

  • GET /v1/projects/:id/leased-accounts, POST /v1/projects/:id/leased-accounts/request, GET /v1/projects/:id/leased-accounts/requests/:requestId, and DELETE /v1/leased-accounts/:accountId. The lease-request workflow was never adopted — zero requests were ever submitted — and Layers-operated accounts now have one model, managed distribution, ordered from the Layers app and the Layers MCP server.
  • Scopes leased:read and leased:write. Keys that still carry them keep working for every other scope they hold; the two values are no longer offered at key creation.
  • Webhook events lease_request.assigned and lease_request.rejected, audit events leased_account.requested and leased_account.released, and the leased_account_provision job kind. No endpoint subscribed to or emitted any of them.
  • The lreq_ id prefix.

GET /v1/projects/:id/social-accounts is unchanged, including its leased filter and the leased field on each row.

2026-08-06 — Ads metrics report imported campaigns only

Changed

  • GET /v1/projects/:id/ads-metrics, GET /v1/projects/:id/ads/ads-metrics, and the paid path of GET /v1/projects/:id/top-performers now report only campaigns explicitly imported into the project — the same semantics the Layers app uses. Previously these endpoints aggregated every campaign on the connected ad account, so a project that imported one campaign out of forty reported all forty. Totals can drop for any project that imported a subset of its account; the numbers now match what the project actually manages. scope=ad requests for ads whose campaign is not imported return 404.

Fixed

  • Meta paid rows were silently absent from top-performers paid rankings (an internal account-id type mismatch dropped every Meta row before ranking). Meta ads now rank alongside TikTok.

2026-07-11 — Apple adset budget: translated to the parent campaign

Fixed

  • PATCH /v1/projects/:id/ads/apple/adgroups/:adgroupId/budget now works end-to-end. Apple Search Ads carries budget at the campaign level only (ad groups have no budget field), so Layers translates the adset-grain patch to the parent campaign's daily budget; lifetimeBudgetCents maps to the campaign's lifetime budgetAmount. Authority still arbitrates at the adset grain. See the Apple note on PATCH …/adsets/:id/budget.

2026-06-09 — Sub-org wallets: allocate returns a transfer; metadata tightened

Changed

  • POST /v1/organizations/:orgId/credits/allocate now returns the created transfer resource instead of a bare balance snapshot: a stable id (txn_<uuid>, the parent-side ledger row — reconcile against it and re-fetch via GET …/credits/events), created, the echoed description/metadata, the child's balance/available, and replayed. An idempotent replay returns the same id + allocated with replayed: true.
  • Partner metadata is now Stripe-shaped across org create/patch and credit allocation: string→string, key ≤ 40 chars, value ≤ 500 chars, ≤ 50 keys (16 KB backstop). Out-of-bounds → 422 VALIDATION.

Fixed

  • PATCH /v1/organizations/:orgId metadata merges key-by-key (Stripe-style) again — send only the keys you're changing; unset one with an empty string (""); metadata: null clears all. (Per-key null is retired; use "".)

2026-06-03 — Sub-organizations

Run customers as isolated child organizations under your parent org, the Stripe Connect / Twilio Subaccounts shape. Additive and non-breaking — the flat one-project-per-customer model is unchanged. See the walkthrough guide.

Added

  • Organization control plane. New org:admin-gated endpoints to create, read, list, patch, suspend, resume, and archive child orgs (/v1/organizations, /v1/organizations/:orgId, …/suspend, …/resume). Each child is an isolation boundary with its own wallet, audit log, projects, and keys. Hierarchy is one level deep. New scope: org:admindeny-by-default (must be granted explicitly; legacy unscoped and partner-tier keys do not get it for free).
  • Act on behalf of a child. The X-Layers-Organization header lets an org:admin parent key run any existing endpoint inside a direct child. See Authentication.
  • Child API keys. Mint, list, rotate, and revoke least-privilege keys bound to a single child (/v1/organizations/:orgId/api-keys). Scopes must be a subset of the parent's; org:admin is never delegable to a child. The secret is shown once on mint/rotate. Rotation is zero-downtime — the old secret keeps working for a 24-hour grace window, then stops. /v1/whoami now returns parentOrganizationId, and apiKeyId is emitted in the key_-prefixed form.
  • Per-child wallets + allocation. Fund a child from the parent wallet with POST /v1/organizations/:orgId/credits/allocate (idempotent). Read a child's wallet with GET …/credits and ledger with GET …/credits/events. Archiving a child reclaims its unspent (non-reserved) credits to the parent (reclaimedCredits on the archive response).
  • Per-child spend caps + auto-refill, set via the API. Read and set a child's monthly cap and auto-refill rule with GET / PATCH /v1/organizations/:orgId/credit-configmonthlyCreditCap, refillThreshold, refillAmount (all in credits, nullable) plus the derived autoRefillEnabled. A capped child's over-cap generation is denied with 402 BILLING_EXHAUSTED (details.reason: "cap"); auto-refill tops a child up from the parent when its available drops below the threshold (a capped child is never refilled past its ceiling). Auto-refill is both-or-neither (422 REFILL_REQUIRES_THRESHOLD_AND_AMOUNT). The config also appears on the GET /:orgId summary as creditConfig. See the Credits concept.
  • Webhook firehose (scope: "all_children"). A parent org:admin key can register one webhook endpoint that receives every direct child's events alongside its own, each attributable via data.organizationId. Defaults to own; setting all_children requires org:admin.
  • Migrate flat projects into children. POST /v1/organizations/migrate atomically creates child orgs and moves existing projects under them from a { projectId: childOrgName } mapping. Historical credit events stay on the parent; migrated projects keep a 30-day parent read-grace.

Changed

  • available + reservedCredits on the credits wallet. GET /v1/credits now returns available (gross balance net of credits reserved for in-flight generations) and reservedCredits alongside the existing fields. Gate generate decisions on available. Purely additive — existing fields are unchanged.
  • New allocation event type on your own ledger. GET /v1/credits/events can now return events with eventType: "allocation" — the parent side of a parent→child transfer (negative when you fund a child, positive when an archived child's unspent credits are reclaimed). metadata.direction is allocate or reclaim, metadata.counterpartyOrgId (org_-prefixed) names the child, and metadata.transferId (txn_-prefixed) joins to the allocate response id. If your integration enumerates or switches on eventType, add a branch for allocation so transfers aren't dropped from your reconciliation.
  • data.organizationId on every webhook payload. Webhook delivery envelopes now include data.organizationId (the prefixed id of the org the event belongs to) on all events, not just firehose ones. Purely additive — existing data.* consumers are unaffected; it's what makes the all_children firehose attributable.

2026-05-08 — API integration redesign (the "first design partner" round)

End-to-end redesign in response to the first design-and-build partner's wishlist. The cross-cutting principle is docs are the contract — every change here lands in code at the same time the docs claim it.

Added

  • Preview object on every content surface. Every container response now carries a normalized preview object with kind, primaryUrl, thumbnailUrl, imageUrls, videoUrl, hlsUrl, durationMs, aspectRatio. Same shape on get / list / job-completion / content.generated webhook. Backed by new content_containers columns + ffprobe + poster-frame extraction activities.
  • Multi-variant fan-out. variantCount on POST /v1/projects/:id/content accepts 1–5. Response is containerIds[] plural; billing scales linearly.
  • Hook is the only call-time creative input. POST /v1/projects/:id/content takes a top-level required hook string, used verbatim in slide 1 / overlay #1. Brand voice, language, audience, and influencer roster are drawn from project + layer state via PATCH /v1/projects/:id and the layer / influencer endpoints — no brief object at the partner surface. Pull a vetted hook from the new GET /v1/projects/:id/content/hooks endpoint or send your own.
  • Partner asset upload. New app-media endpointsPOST /v1/projects/:id/app-media (upload from a public URL), GET …, DELETE …/:mediaId — with per-kind mime + byte-cap validation for logo / screenshot / demo-video. URL-based fetch through the SSRF guard; no presign / finalize dance at the partner edge. Scope: projects:write. Ungates ugc-remix partner-callability.
  • Ads write surface. Full mirror of admin paid-media/* routes under /v1/projects/:id/ads/.... New scopes: ads:write:campaigns, :budgets, :creative, :lifecycle, :policy, :optimizer_trigger, :pending, :capi (existing ads:write:connect, ads:write:override retained). Headline simplified create-campaign endpoint (budget + outcome + management mode + creative mode). Cents universally on every money field. Apple campaign-create lifted (see LOCK 10 investigation). 15 webhook events under ads.* and approval.*.
  • TikTok admin parity. paid-media/tiktok/{authority,defaults,kill-switch,pending,creatives} admin routes shipped to match Meta + Apple. Required for symmetric partner write surface.
  • SDK platform support. nextjs, vite, react-router added to the SDK app platform enum. New install-spec generator branches with framework-specific filesToCreate. New Next.js, Vite, React Router guides.
  • Server-side event forwarding. New POST /v1/events — partner-API surface for batch event ingest, distinct from the SDK runtime path. New scope: events:write.
  • Active tracking probe. New POST …/sdk-apps/:appId/verify-tracking — writes a synthetic event into Layers and reads it back, reporting SDK install health plus whether each CAPI destination is configured. The synthetic event is never forwarded to an ad platform. Companion SDK health concept page. (Corrected 2026-08-15: this entry originally promised "per-platform CAPI dispatch confirmation", which the endpoint has never returned.)
  • CAPI per-app config wiring (CF-3 fix). PATCH /v1/projects/:id/sdk-apps/:appId now writes through to project_layers.config.capi (the relay's read path). Previously the partner toggle was disconnected — every CAPI claim in docs is now honest.
  • Audit log for partners. GET /v1/audit-log now returns partner-attributable rows (actor_type: "partner") with the partner's actor_organization_id and actor_api_key_id populated. Every gate decision logs a verdictId returnable on the partner write response. Backed by migration 20260508000000_partner_write_safety.sql (adds 'partner' to the layer_ads_audit.actor_type CHECK, plus actor_organization_id + actor_api_key_id on layer_ads_audit and optimizer_pending_actions with partial indexes).
  • Approval dispatch observability. New dispatched_at + dispatched_failure_reason columns on optimizer_pending_actions (same migration), wired to the new approval.dispatched / approval.dispatch_failed webhooks. Partners reading GET …/pending see the platform-side dispatch timing inline.
  • Wildcard scopes. hasScope() (api/lib/partner/scope.ts) now expands <resource>:<action>:* to cover every sub-scope under that pair — ads:write:* covers all eight new sub-scopes. Existing <resource>:* and * wildcards continue to work. See API keys → wildcard scopes.
  • Comprehensive ads webhook set. All 15 events in Phase 1 (vs. the bare-min 6): ads.account.{connected,disconnected,token_expired}, ads.optimizer.{action,run.completed}, ads.write.{executed,denied}, ads.creative.{flagged,unlocked}, ads.policy.violation, approval.{dispatched,dispatch_failed,approved,disapproved}, ads.budget.cap_exceeded.
  • First-party React example app. A react-quickstart walkthrough — full happy path (bootstrap → influencer → generate → poll → gallery → approve → connect → publish → metrics). (Example app docs are being rebuilt; the page is temporarily unavailable.)

Changed

  • Format enum. Partner enum is now slideshow-builder | ugc-remix (v1 launch set). auto is removed — partners must name a type. video-remix | slideshow-remix are reserved for v2 (return UNSUPPORTED_FORMAT in v1) — the surface to pass / discover a third-party source post is not yet exposed. See content items → Roadmap: source-coupled formats. Naming honesty: slideshow-builder-remix is renamed to slideshow-builder at the partner surface (the -remix suffix was inaccurate; nothing was being remixed).
  • Idempotency-Key is the canonical retry mechanism. Body id is rejected with 422 VALIDATION on every create endpoint (project, sdk-app, influencer, content). The previous "partner-created IDs as alternative" path is gone. See Idempotency.
  • references lives at the top level. Top-level references = { mediaIds?, assetIds? } (each capped at 20). Pass media-library handles via references.mediaIds[].
  • Cents universally for ads. Every budget / bid / cap field on the partner ads contract is integers in cents. Layers translates per-platform at the proxy boundary (Meta pass-through, TikTok / 100, Apple {amount: <decimal>, currency: ISO}).
  • ads:write legacy scope. Mapped to the union of new sub-scopes for backwards compat. New keys should mint specific sub-scopes.
  • Pricing page enum. slideshow-builder 50 credits / ugc-remix 120. Multi-variant scales linearly.

Removed

  • auto format. Was non-deterministic plumbing pretending to be a product feature.
  • refuse_write_without_gate_trg SQL trigger (Meta + Apple variants) — previously DEFAULT-OFF backstop; the TS gate + no-ungated-platform-write ESLint rule are the only active enforcement. Cleanup in migration 20260507000000_drop_legacy_authority_objects.sql (PR #1778).
  • "Read-only today" callouts on reference/ads/list-{campaigns,adsets,ads}.mdx. Replaced with bucket-mode authority + sub-scope pointers.

Fixed

  • Misleading 409 message on duplicate content-create — body id is now rejected outright; see Changed above.
  • Apple campaign-create unconditional deny. Stale Layers product policy (the gate comment claimed Apple's API didn't support it; Apple's API does, and our internal create-agent already uses the endpoint). Lift documented in LOCK 10 investigation.
  • Apple ad-level entity mismatch — partner endpoints never expose …/apple/.../ads; Apple's API has no ad-level entity. Use …/apple/adgroups/:agid/keywords for tier-3 (D36).

2026-04-24 - Preview release

First public release.

Added

  • GET /v1/whoami - resolve an API key to its organization, scopes, and rate-limit tier.
  • Projects: create, list, get, patch, archive. customer_external_id for sub-tenant scoping.
  • GitHub install + ingest: register an installation, list visible repos, kick off POST /v1/projects/:id/ingest/github to generate and open an SDK-install PR.
  • SDK apps: create, get, patch. CAPI status per app for Meta, TikTok, Apple.
  • Deterministic install-spec generator for non-GitHub installs (iOS SPM, Android Gradle, web npm, react-native, flutter, expo).
  • Event stream query + per-user signal endpoint, project-scoped and PII-redacted by default.
  • Conversions rollup and Apple Ads attribution records, project-scoped.
  • Influencers: create (async), clone (sync, from image URL or existing), patch, archive, bind reference media.
  • Content: generate (async), read, list with filters, signed asset URLs.
  • Approval: POST /v1/content/:id/approve / reject, per-project requires_approval + first_n_posts_blocked policy, scheduled-posts gate enforcement.
  • Social OAuth with partner-owned returnUrl (allowlisted); status-poll endpoint; account list, reauth URL, revoke.
  • Scheduling + publishing across connected accounts, with approval gate enforced pre-publish.
  • Leased TikTok accounts: request endpoint (queued for Layers to fulfill manually), list, release. Provisioning remains a manual admin function - permanently.
  • Engagement config read + patch — v1 (enabled, firstComment.{targets,commentTemplate}, replyToComments.{targets,autoReplyDelay}) and v2 (firstComment.strategy, replyToComments.{tone,maxPerPostPerHour,ignoreCommentsMatching,escalateNegativeSentiment}) wired end-to-end. Former 403 FORBIDDEN_FENCE is removed.
  • Metrics: unified organic metrics, ads metrics, top-performers ranking, scored ads-content list with override PATCH.
  • Ads reads: campaigns, ad-sets, ads across Meta, TikTok, Apple. CRUD is planned.
  • Unified jobs envelope: GET /v1/jobs/:jobId, POST /v1/jobs/:jobId/cancel. One pattern covers every long-running operation.
  • Idempotency-Key on every mutating POST, with partner-created resource IDs as a stronger alternative.
  • Rate limits on pilot / gic_pilot / gic_ga tiers, per-endpoint-class buckets.
  • Kill switch - per-key, per-org, and global.
  • Stable error codes with requestId on every response.

Known gaps

  • Ads campaign, ad-set, and ad writes - planned. Reads are live.
  • Trust-score approval auto-transition - planned. Today you flip requires_approval manually or lean on first_n_posts_blocked.
  • Webhooks - live. HMAC-SHA256 signed, 8-attempt retry ladder, dedupe + replay. See Webhooks. Polling remains supported; webhooks are an optimization, not a replacement.
  • Defense-in-depth on every publish path - gated at schedule + scheduled-posts-publish-due today; IG / TikTok / Managed publish paths get rechecked in a future release.
  • LinkedIn + YouTube social platforms - not committed; gated on platform prioritization.

On this page