Layers
Partner APIConcepts

API keys

Key format, scope taxonomy, rate-limit tiers, and revocation paths.

View as Markdown

An API key authenticates every request you make and decides what that request is allowed to do. One key is scoped to one organization. It carries the rate-limit tier that caps throughput, the allowlist of returnUrl values the OAuth flows will accept, and the scope list that gates individual routes.

Key format

lp_<env>_<keyid>_<secret>

The env segment is one of live or test:

  • lp_live_… — production. Real platform calls, real credit burn, real OAuth.
  • lp_test_… — sandbox. Deterministic fakes, no credit burn, no real platform calls. Identical response envelopes and signed webhooks. See Sandbox for the full behavior contract.

The two are minted separately. Routes don't gate on env — the same scope set works on either — but the request envelope's behavior diverges as soon as a sandbox key is detected. Any prefix-vs-row env mismatch is rejected at auth time with 401 UNAUTHENTICATED.

Treat the full key as an opaque secret beyond the env segment. Layers may expose a shortened key id in responses and audit logs; that id is safe to log, but the full key is not.

The secret is shown exactly once - at the moment of creation. We can't recover it, display it again, or confirm you have the right one. Paste it into your secrets manager before you close the tab.

Sending the key

Send the key as Authorization: Bearer <key> on every request. This is the only accepted auth form.

GET /v1/whoami HTTP/1.1
Host: api.layers.com
Authorization: Bearer lp_...

The first call any client should make is GET /v1/whoami - it resolves your key to the org it's bound to and lets you fail fast if anything is wrong. Response shape:

{
  "organizationId": "org_2481fa5c-a404-44ed-a561-565392499abc",
  "workspaceId": "org_2481fa5c-a404-44ed-a561-565392499abc",
  "organizationName": "Acme Growth",
  "scopes": [],
  "parentOrganizationId": null,
  "rateLimitTier": "standard",
  "apiKeyId": "key_c2037bb9-354d-4662-96b7-97a28ad6b6e1"
}

parentOrganizationId names the parent org a child key reports to (or null for a top-level key); scopes is the key's granted scopes as a flat string array. See whoami for the full field reference.

A 200 from /whoami is itself the "key is live and the org is in good standing" signal. If access is suspended every endpoint - including this one - returns 503 KILL_SWITCH with a request id; quote that id to your Layers contact.

Scopes

Scope enforcement is live and deny-by-default. A key is granted only the scopes minted onto it; calling an endpoint whose declared scope the key doesn't hold returns 403 FORBIDDEN_SCOPE. An empty scope list grants nothing — every new key must be minted with at least one scope (see Creating API keys). /v1/whoami returns whichever scopes were minted onto the key.

Every scope and the functionality it gates:

ScopeFunctionality
projects:readList and read projects.
projects:writeCreate and update projects.
ingest:writeTrigger GitHub / website / App Store ingestion jobs.
content:readList and read generated content.
content:writeStart content generation (slideshow / video / UGC remix).
content:approveApprove or reject generated content.
social:readList connected social accounts.
social:writeConnect, configure, and remove social accounts (OAuth flows).
publish:readList scheduled posts and their status.
publish:writeSchedule and publish content to connected accounts.
events:readRead the SDK analytics event stream.
events:read+piiRead events including unredacted personally-identifiable fields. Grant only when the integration needs PII.
events:writeForward server-side events / CAPI relay (counts against the customer's analytics + relay).
metrics:readRead organic and ads performance metrics.
ads:readRead ad accounts, campaigns, ad sets, ads, metrics, audit log, and the pending queue.
ads:writeUmbrella ads-write — OAuth init + ads-content override. Prefer the fine-grained ads:write:* sub-scopes below for least-privilege keys.
bootstrap:writeRun the end-to-end marketing-bootstrap onboarding workflow.
media:readList and read media-library assets.
media:writeUpload, finalize, and delete media-library assets.
influencers:readList and read influencers.
influencers:writeCreate, clone, and patch influencers.
leased:readList and read leased-account requests.
leased:writeSubmit and release leased-account requests.
engagement:readRead auto-pilot engagement config.
engagement:writePatch auto-pilot engagement config.
credits:readRead the org credit balance, ledger, and per-format estimated costs.
github:adminRegister and read GitHub App installations.
jobs:readRead async job status.
jobs:cancelCancel running async jobs.
webhooks:writeRegister, rotate, replay, and delete webhook endpoints (read-side shares this scope today).
org:adminThe sub-organization control plane: create / list / get / patch / suspend / resume / archive child organizations, mint child keys, allocate credits, and act on a child via the X-Layers-Organization header. Deny-by-default + non-delegable — see below.

org:admin is deny-by-default and non-delegable. It governs minting customer orgs, draining wallets, and offboarding, so a key must carry it explicitly — no wildcard confers it (not even *), partner/internal-tier keys do not bypass it, and it can never be granted to a child key. It is minted only through the organization-management key flow, never selected alongside ordinary data scopes.

Parent and child orgs

When you run customers as child organizations, your top-level (parent) key carries org:admin. To operate inside a child — create a project for that customer, read its credits — send the X-Layers-Organization header with the parent key. A child that isn't a direct child of your org resolves to 404.

When you'd rather give a customer its own credential instead of driving it from the parent, mint a child-scoped key (below): a least-privilege key bound to one child org, with zero-downtime rotation.

Pending scope additions (this release)

The following scopes are planned for the partner API redesign and are documented on the surfaces that depend on them; they are not yet in the canonical PARTNER_API_SCOPES enum at the moment this page lands. Until they ship, the listed routes use the closest existing scope (typically projects:write for resource-create routes, ads:write for ads CAPI). Track the changelog for the exact ship date.

ScopeWill gateStatus
sdk:read / sdk:writeSDK app CRUD + verify-trackingPending — covered by projects:write / projects:read until shipped.

Ads sub-scopes

The ads:* family is fine-grained. Mint only the sub-scopes the partner integration actually uses. Each corresponds to a specific endpoint class on the ads write surface.

ScopeGates
ads:readReading campaigns, ad sets, ads, metrics, audit log, pending queue.
ads:write:campaignsCreate / patch / delete campaigns. The simplified Create-campaigns endpoint requires this.
ads:write:budgetsPATCH …/campaigns/:id/budget, …/adsets/:id/budget, daily/lifetime cap mutations.
ads:write:creativePush / replace / prune ad creatives.
ads:write:lifecyclePause / resume / archive across the campaign / adset / ad hierarchy.
ads:write:policyAuthority CRUD (PATCH …/campaigns/:id/authority, …/adgroups/:id/authority). Customer's own bucket-mode controls remain authoritative — this scope governs partner-issued authority changes inside the customer's policy.
ads:write:optimizer_triggerPOST …/optimizer/run and the per-layer write-class flags.
ads:write:pendingApprove / reject pending optimizer actions.
ads:write:capiPatch a project's CAPI config (Meta + TikTok). Wires through to project_layers.config.capi.

The legacy single ads:write scope is mapped to the union of the sub-scopes above for backwards compatibility on existing keys; new keys should mint sub-scopes explicitly.

There is no ads:write:kill_switch scope. Kill-switch is customer-only; partner keys cannot trip it. Partners observe the kill-switch state passively via the audit log and the ads.write.denied webhook.

Wildcard scopes

The scope check accepts three wildcard forms — useful when minting an internal-tier key that should pass any future scope your endpoints adopt without re-issuing the key:

WildcardCovers
*Every scope except the non-delegable org:admin, which always requires an exact grant. Reserved for internal/admin keys.
<resource>:*Every action on a resource. Example: ads:* covers ads:read, ads:write, every ads:write:<sub>, etc.
<resource>:<action>:*Every sub-scope under a resource:action pair. Example: ads:write:* covers all eight ads:write:* sub-scopes but does not cover ads:read.

Wildcards expand at request time via hasScope() (api/src/lib/partner/scope.ts); they don't pre-mint sub-scopes onto the key. /v1/whoami returns the wildcard string verbatim — partners reading scope strings programmatically should evaluate wildcards rather than expecting expansion.

Empty scope lists are denied. A key with an empty/missing scopes array now grants nothing (deny-by-default) — it is not a "full access" passthrough. Keys that pre-date this change were migrated to the explicit * (god-mode) sentinel so they keep their prior access; every new key must be minted with at least one scope. Even the * god-mode wildcard does not confer the non-delegable org:admin — that scope requires an exact grant.

Creating API keys

Every key carries an explicit set of scopes chosen when the key is created. Because enforcement is deny-by-default, you must select at least one scope — a scope-less key can't call anything.

There are two ways to create a key:

  • Your own organization's key — from the Layers dashboard under Org Settings → API Keys. The creation dialog has an expandable Permissions section: pick the scopes this key needs, then create. The full key secret is shown once on creation — copy it immediately.
  • A per-customer child key — mint it programmatically against a child org with your parent (org:admin) key via POST /v1/organizations/:orgId/api-keys. The request requires a non-empty scopes array (a subset of your own key's scopes); see child keys.

Choosing scopes

Grant the least privilege the integration needs — read scopes for read-only consumers, the matching :write scope only where the key actually mutates state. The scope table above lists every scope and what it gates. org:admin is never selectable here: it is minted only through the organization-management key flow and is never delegable to a child.

Scope is one-way

A key's scopes are fixed at creation and cannot be edited afterward. There is no "change permissions" operation — narrowing or widening a key's access means minting a new key with the desired scopes and revoking (or rotating out) the old one. This keeps a key's authority auditable and immutable for its whole lifetime; key lists display the granted scopes read-only.

Rate-limit tiers

Tier is a property of the key, not the endpoint. Buckets are keyed per (key_id, endpoint_class) so a runaway generation loop can't starve your reads. Endpoint classes: read-light, write-light, long-running.

TierTypical provisioning
standardDefault for every partner key.
pilotHigher throughput for early-integration partners - granted by Layers on request.
partnerEnterprise tier for GA partners with SLAs.

Every 429 response carries:

  • Retry-After
  • X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset
  • X-RateLimit-Endpoint-Class, X-RateLimit-Tier
  • Body with error.details.retryAfterMs and error.details.endpointClass

See common patterns for a back-off snippet and rate limits for the full bucket policy.

Child keys for sub-organizations

If you manage customers as sub-organizations, you can mint API keys directly for each child org with your parent (org:admin) key. A child key is a per-customer credential: it's bound to exactly one child org and carries exactly the access you grant it.

Two rules govern what a child key may hold:

  • Scope subsetting. A child key can only carry scopes you already hold. Request a scope your parent key doesn't have and the mint is rejected with 403 FORBIDDEN_SCOPE (details.offendingScopes names the offenders). You can never grant a child more than you have.
  • org:admin is never delegable. Even if your parent key holds org:admin (or a * wildcard), a child key can never receive it. The child must not run the control plane - minting its own children, moving projects, or draining wallets. Requesting org:admin for a child returns 403 FORBIDDEN_SCOPE.

The full lifecycle is parent-driven and lives under the child org's path:

  • Mint - POST /v1/organizations/:orgId/api-keys. Secret returned once.
  • List - masked; never the secret.
  • Rotate - zero-downtime, see below.
  • Delete - immediate revoke.

These routes are anti-enumeration: a :orgId that isn't your direct child, or a :keyId that isn't that child's, both return 404 NOT_FOUND - a stranger's resource looks identical to a missing one. A malformed id returns 422 VALIDATION.

Rotation

You can rotate your own (parent / top-level) key without downtime:

Ask Layers to create a second key with the same access.
Deploy the new secret to your callers.
Confirm callers have stopped using the old key.
Revoke the old key.

Both keys are active in parallel during the cutover, so there's no flap.

Rotation grace window

Child keys rotate through a built-in API instead of a manual two-key dance. POST …/api-keys/:keyId/rotate mints a fresh key (new id, new secret, same scopes) and puts the old secret into a 24-hour grace window so there's no downtime while you roll out the new one:

Rotate. You get the new key + its one-time secret back in the response.
Deploy the new secret across the customer's fleet within 24 hours. The old secret keeps authenticating the whole time, so nothing flaps.
The old secret stops automatically at the deadline. List the child's keys to see it - the old row carries graceUntil (when it dies) and supersededBy (the new key's id). Once graceUntil passes, the old secret returns 401 UNAUTHENTICATED.

To cut the old secret off before the window closes, delete the old key - that's an immediate, grace-free revoke. A key rotates once: the rotation chain rolls forward (K → K2 → K3), and rotating an already-superseded key returns 409 CONFLICT - always rotate the current key.

The grace window never overrides a kill switch. A child key - new or old, in grace or not - is rejected with 503 KILL_SWITCH the moment its org is suspended or archived, or a per-key kill switch is flipped. Grace only postpones an old secret's normal expiry; it never resurrects access that ops has cut.

Revocation

Three levers, each stronger than the last:

  • Revoke the key. Every subsequent request fails with 401 UNAUTHENTICATED. This is the normal path.
  • Kill switch on the key. Requests fail with 503 KILL_SWITCH - a different signal than revoked, so you can tell an incident from a rotation. Clearable without reissuing.
  • Org-wide kill switch. Every key on the org fails. Reserved for incident response.

Revocation and kill-switch flips take effect on the next request - propagation is effectively immediate.

On this page