Layers
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)
  • Leased-account request-flow visibility: richer status vocab (requestedin_reviewprovisioningassignedfailed), webhook: lease_request.assigned.
  • 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-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 — Partner API 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 — sends a synthetic event tagged test_event_code and reports per-platform CAPI dispatch confirmation. Companion SDK health concept page.
  • 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. /docs/api/examples/react-quickstart — full happy path (bootstrap → influencer → generate → poll → gallery → approve → connect → publish → metrics).

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